use std::{collections::BTreeMap, sync::Arc};
use derive_more::{Display, From};
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
#[cfg(feature = "unstable_plan_operations")]
use super::PlanRemoved;
#[cfg(feature = "unstable_elicitation")]
use super::{
CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse,
ElicitationCapabilities,
};
use super::{
ContentBlock, ExtNotification, ExtRequest, ExtResponse, Meta, PlanUpdate, SessionConfigOption,
SessionId, ToolCall, ToolCallUpdate,
};
use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener};
#[cfg(feature = "unstable_mcp_over_acp")]
use super::mcp::{
ConnectMcpRequest, ConnectMcpResponse, DisconnectMcpRequest, DisconnectMcpResponse,
MCP_CONNECT_METHOD_NAME, MCP_DISCONNECT_METHOD_NAME, MCP_MESSAGE_METHOD_NAME,
MessageMcpNotification, MessageMcpRequest, MessageMcpResponse,
};
#[cfg(feature = "unstable_nes")]
use super::{ClientNesCapabilities, PositionEncodingKind};
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SessionNotification {
pub session_id: SessionId,
pub update: SessionUpdate,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl SessionNotification {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
Self {
session_id: session_id.into(),
update,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
#[schemars(extend("discriminator" = {"propertyName": "sessionUpdate"}))]
#[non_exhaustive]
pub enum SessionUpdate {
UserMessageChunk(ContentChunk),
AgentMessageChunk(ContentChunk),
AgentThoughtChunk(ContentChunk),
ToolCall(ToolCall),
ToolCallUpdate(ToolCallUpdate),
PlanUpdate(PlanUpdate),
#[cfg(feature = "unstable_plan_operations")]
PlanRemoved(PlanRemoved),
AvailableCommandsUpdate(AvailableCommandsUpdate),
ConfigOptionUpdate(ConfigOptionUpdate),
SessionInfoUpdate(SessionInfoUpdate),
UsageUpdate(UsageUpdate),
#[serde(untagged)]
Other(OtherSessionUpdate),
}
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq)]
#[schemars(inline)]
#[schemars(transform = other_session_update_schema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OtherSessionUpdate {
#[serde(rename = "sessionUpdate")]
pub session_update: String,
#[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>,
}
impl OtherSessionUpdate {
#[must_use]
pub fn new(
session_update: impl Into<String>,
mut fields: BTreeMap<String, serde_json::Value>,
) -> Self {
fields.remove("sessionUpdate");
Self {
session_update: session_update.into(),
fields,
}
}
}
impl<'de> Deserialize<'de> for OtherSessionUpdate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
let session_update = fields
.remove("sessionUpdate")
.ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
let serde_json::Value::String(session_update) = session_update else {
return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
};
if is_known_session_update(&session_update) {
return Err(serde::de::Error::custom(format!(
"known session update `{session_update}` did not match its schema"
)));
}
Ok(Self {
session_update,
fields,
})
}
}
fn is_known_session_update(session_update: &str) -> bool {
matches!(
session_update,
"user_message_chunk"
| "agent_message_chunk"
| "agent_thought_chunk"
| "tool_call"
| "tool_call_update"
| "plan_update"
| "available_commands_update"
| "config_option_update"
| "session_info_update"
| "usage_update"
)
}
fn other_session_update_schema(schema: &mut Schema) {
super::schema_util::reject_known_string_discriminators(
schema,
"sessionUpdate",
&[
"user_message_chunk",
"agent_message_chunk",
"agent_thought_chunk",
"tool_call",
"tool_call_update",
"plan_update",
"available_commands_update",
"config_option_update",
"session_info_update",
#[cfg(feature = "unstable_plan_operations")]
"plan_removed",
"usage_update",
],
);
}
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ConfigOptionUpdate {
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
pub config_options: Vec<SessionConfigOption>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl ConfigOptionUpdate {
#[must_use]
pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
Self {
config_options,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[skip_serializing_none]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SessionInfoUpdate {
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub title: MaybeUndefined<String>,
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub updated_at: MaybeUndefined<String>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl SessionInfoUpdate {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
self.title = title.into_maybe_undefined();
self
}
#[must_use]
pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
self.updated_at = updated_at.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UsageUpdate {
pub used: u64,
pub size: u64,
#[serde_as(deserialize_as = "DefaultOnError")]
#[schemars(extend("x-deserialize-default-on-error" = true))]
#[serde(default)]
pub cost: Option<Cost>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl UsageUpdate {
#[must_use]
pub fn new(used: u64, size: u64) -> Self {
Self {
used,
size,
cost: None,
meta: None,
}
}
#[must_use]
pub fn cost(mut self, cost: impl IntoOption<Cost>) -> Self {
self.cost = cost.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Cost {
pub amount: f64,
pub currency: String,
}
impl Cost {
#[must_use]
pub fn new(amount: f64, currency: impl Into<String>) -> Self {
Self {
amount,
currency: currency.into(),
}
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ContentChunk {
pub content: ContentBlock,
pub message_id: MessageId,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl ContentChunk {
#[must_use]
pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
Self {
content,
message_id: message_id.into(),
meta: None,
}
}
#[must_use]
pub fn message_id(mut self, message_id: impl Into<MessageId>) -> Self {
self.message_id = message_id.into();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct MessageId(pub Arc<str>);
impl MessageId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AvailableCommandsUpdate {
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
pub available_commands: Vec<AvailableCommand>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl AvailableCommandsUpdate {
#[must_use]
pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
Self {
available_commands,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AvailableCommand {
pub name: String,
pub description: String,
#[serde_as(deserialize_as = "DefaultOnError")]
#[schemars(extend("x-deserialize-default-on-error" = true))]
#[serde(default)]
pub input: Option<AvailableCommandInput>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl AvailableCommand {
#[must_use]
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
input: None,
meta: None,
}
}
#[must_use]
pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
self.input = input.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged, rename_all = "camelCase")]
#[non_exhaustive]
pub enum AvailableCommandInput {
Unstructured(UnstructuredCommandInput),
Other(OtherAvailableCommandInput),
}
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
#[schemars(inline)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OtherAvailableCommandInput {
#[serde(rename = "type")]
pub type_: String,
#[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>,
}
impl OtherAvailableCommandInput {
#[must_use]
pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
fields.remove("type");
Self {
type_: type_.into(),
fields,
}
}
}
impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
let type_ = fields
.remove("type")
.ok_or_else(|| serde::de::Error::missing_field("type"))?;
let serde_json::Value::String(type_) = type_ else {
return Err(serde::de::Error::custom("`type` must be a string"));
};
Ok(Self { type_, fields })
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
#[schemars(transform = unstructured_command_input_schema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UnstructuredCommandInput {
pub hint: String,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl UnstructuredCommandInput {
#[must_use]
pub fn new(hint: impl Into<String>) -> Self {
Self {
hint: hint.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
impl<'de> Deserialize<'de> for UnstructuredCommandInput {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawUnstructuredCommandInput {
hint: String,
#[serde(rename = "_meta")]
meta: Option<Meta>,
#[serde(flatten)]
fields: BTreeMap<String, serde_json::Value>,
}
let raw = RawUnstructuredCommandInput::deserialize(deserializer)?;
if raw.fields.contains_key("type") {
return Err(serde::de::Error::custom(
"unstructured command input cannot include a `type` field",
));
}
Ok(Self {
hint: raw.hint,
meta: raw.meta,
})
}
}
fn unstructured_command_input_schema(schema: &mut Schema) {
super::schema_util::reject_property(schema, "type");
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RequestPermissionRequest {
pub session_id: SessionId,
pub tool_call: ToolCallUpdate,
pub options: Vec<PermissionOption>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl RequestPermissionRequest {
#[must_use]
pub fn new(
session_id: impl Into<SessionId>,
tool_call: ToolCallUpdate,
options: Vec<PermissionOption>,
) -> Self {
Self {
session_id: session_id.into(),
tool_call,
options,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PermissionOption {
pub option_id: PermissionOptionId,
pub name: String,
pub kind: PermissionOptionKind,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl PermissionOption {
#[must_use]
pub fn new(
option_id: impl Into<PermissionOptionId>,
name: impl Into<String>,
kind: PermissionOptionKind,
) -> Self {
Self {
option_id: option_id.into(),
name: name.into(),
kind,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct PermissionOptionId(pub Arc<str>);
impl PermissionOptionId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PermissionOptionKind {
AllowOnce,
AllowAlways,
RejectOnce,
RejectAlways,
#[serde(untagged)]
Other(String),
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RequestPermissionResponse {
pub outcome: RequestPermissionOutcome,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl RequestPermissionResponse {
#[must_use]
pub fn new(outcome: RequestPermissionOutcome) -> Self {
Self {
outcome,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(tag = "outcome", rename_all = "snake_case")]
#[schemars(extend("discriminator" = {"propertyName": "outcome"}))]
#[non_exhaustive]
pub enum RequestPermissionOutcome {
Cancelled,
#[serde(rename_all = "camelCase")]
Selected(SelectedPermissionOutcome),
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SelectedPermissionOutcome {
pub option_id: PermissionOptionId,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl SelectedPermissionOutcome {
#[must_use]
pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
Self {
option_id: option_id.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ClientCapabilities {
#[cfg(feature = "unstable_auth_methods")]
#[serde(default)]
pub auth: AuthCapabilities,
#[cfg(feature = "unstable_elicitation")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[schemars(extend("x-deserialize-default-on-error" = true))]
#[serde(default)]
pub elicitation: Option<ElicitationCapabilities>,
#[cfg(feature = "unstable_nes")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[schemars(extend("x-deserialize-default-on-error" = true))]
#[serde(default)]
pub nes: Option<ClientNesCapabilities>,
#[cfg(feature = "unstable_nes")]
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub position_encodings: Vec<PositionEncodingKind>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl ClientCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "unstable_auth_methods")]
#[must_use]
pub fn auth(mut self, auth: AuthCapabilities) -> Self {
self.auth = auth;
self
}
#[cfg(feature = "unstable_elicitation")]
#[must_use]
pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
self.elicitation = elicitation.into_option();
self
}
#[cfg(feature = "unstable_nes")]
#[must_use]
pub fn nes(mut self, nes: impl IntoOption<ClientNesCapabilities>) -> Self {
self.nes = nes.into_option();
self
}
#[cfg(feature = "unstable_nes")]
#[must_use]
pub fn position_encodings(mut self, position_encodings: Vec<PositionEncodingKind>) -> Self {
self.position_encodings = position_encodings;
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_auth_methods")]
#[serde_as]
#[skip_serializing_none]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AuthCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[schemars(extend("x-deserialize-default-on-error" = true))]
#[serde(default)]
pub terminal: Option<TerminalAuthCapabilities>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_auth_methods")]
impl AuthCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
self.terminal = terminal.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_auth_methods")]
#[skip_serializing_none]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[non_exhaustive]
pub struct TerminalAuthCapabilities {
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_auth_methods")]
impl TerminalAuthCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ClientMethodNames {
pub session_request_permission: &'static str,
pub session_update: &'static str,
#[cfg(feature = "unstable_mcp_over_acp")]
pub mcp_connect: &'static str,
#[cfg(feature = "unstable_mcp_over_acp")]
pub mcp_message: &'static str,
#[cfg(feature = "unstable_mcp_over_acp")]
pub mcp_disconnect: &'static str,
#[cfg(feature = "unstable_elicitation")]
pub elicitation_create: &'static str,
#[cfg(feature = "unstable_elicitation")]
pub elicitation_complete: &'static str,
}
pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
session_update: SESSION_UPDATE_NOTIFICATION,
session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
#[cfg(feature = "unstable_mcp_over_acp")]
mcp_connect: MCP_CONNECT_METHOD_NAME,
#[cfg(feature = "unstable_mcp_over_acp")]
mcp_message: MCP_MESSAGE_METHOD_NAME,
#[cfg(feature = "unstable_mcp_over_acp")]
mcp_disconnect: MCP_DISCONNECT_METHOD_NAME,
#[cfg(feature = "unstable_elicitation")]
elicitation_create: ELICITATION_CREATE_METHOD_NAME,
#[cfg(feature = "unstable_elicitation")]
elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
};
pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
#[cfg(feature = "unstable_elicitation")]
pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
#[cfg(feature = "unstable_elicitation")]
pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[schemars(inline)]
#[non_exhaustive]
pub enum AgentRequest {
RequestPermissionRequest(RequestPermissionRequest),
#[cfg(feature = "unstable_elicitation")]
CreateElicitationRequest(CreateElicitationRequest),
#[cfg(feature = "unstable_mcp_over_acp")]
ConnectMcpRequest(ConnectMcpRequest),
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpRequest(MessageMcpRequest),
#[cfg(feature = "unstable_mcp_over_acp")]
DisconnectMcpRequest(DisconnectMcpRequest),
ExtMethodRequest(ExtRequest),
}
impl AgentRequest {
#[must_use]
pub fn method(&self) -> &str {
match self {
Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
#[cfg(feature = "unstable_elicitation")]
Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
#[cfg(feature = "unstable_mcp_over_acp")]
Self::ConnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_connect,
#[cfg(feature = "unstable_mcp_over_acp")]
Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
#[cfg(feature = "unstable_mcp_over_acp")]
Self::DisconnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_disconnect,
Self::ExtMethodRequest(ext_request) => &ext_request.method,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[schemars(inline)]
#[non_exhaustive]
pub enum ClientResponse {
RequestPermissionResponse(RequestPermissionResponse),
#[cfg(feature = "unstable_elicitation")]
CreateElicitationResponse(CreateElicitationResponse),
#[cfg(feature = "unstable_mcp_over_acp")]
ConnectMcpResponse(ConnectMcpResponse),
#[cfg(feature = "unstable_mcp_over_acp")]
DisconnectMcpResponse(#[serde(default)] DisconnectMcpResponse),
ExtMethodResponse(ExtResponse),
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpResponse(MessageMcpResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[expect(clippy::large_enum_variant)]
#[schemars(inline)]
#[non_exhaustive]
pub enum AgentNotification {
SessionNotification(SessionNotification),
#[cfg(feature = "unstable_elicitation")]
CompleteElicitationNotification(CompleteElicitationNotification),
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpNotification(MessageMcpNotification),
ExtNotification(ExtNotification),
}
impl AgentNotification {
#[must_use]
pub fn method(&self) -> &str {
match self {
Self::SessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
#[cfg(feature = "unstable_elicitation")]
Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
#[cfg(feature = "unstable_mcp_over_acp")]
Self::MessageMcpNotification(_) => CLIENT_METHOD_NAMES.mcp_message,
Self::ExtNotification(ext_notification) => &ext_notification.method,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialization_behavior() {
use serde_json::json;
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
SessionInfoUpdate {
title: MaybeUndefined::Undefined,
updated_at: MaybeUndefined::Undefined,
meta: None
}
);
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
.unwrap(),
SessionInfoUpdate {
title: MaybeUndefined::Null,
updated_at: MaybeUndefined::Null,
meta: None
}
);
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(
json!({"title": "title", "updatedAt": "timestamp"})
)
.unwrap(),
SessionInfoUpdate {
title: MaybeUndefined::Value("title".to_string()),
updated_at: MaybeUndefined::Value("timestamp".to_string()),
meta: None
}
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
json!({})
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
json!({"title": "title"})
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
json!({"title": null})
);
assert_eq!(
serde_json::to_value(
SessionInfoUpdate::new()
.title("title")
.title(MaybeUndefined::Undefined)
)
.unwrap(),
json!({})
);
}
#[test]
fn test_content_chunk_message_id_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
ContentBlock::Text(crate::v2::TextContent::new("Hello")),
"msg_agent_c42b9",
)))
.unwrap(),
json!({
"sessionUpdate": "agent_message_chunk",
"messageId": "msg_agent_c42b9",
"content": {
"type": "text",
"text": "Hello"
}
})
);
let err = serde_json::from_value::<ContentChunk>(json!({
"content": {
"type": "text",
"text": "Hello"
}
}))
.unwrap_err();
assert!(err.to_string().contains("messageId"), "{err}");
}
#[test]
fn test_usage_update_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
53_000, 200_000
)))
.unwrap(),
json!({
"sessionUpdate": "usage_update",
"used": 53000,
"size": 200000
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::UsageUpdate(
UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
))
.unwrap(),
json!({
"sessionUpdate": "usage_update",
"used": 53000,
"size": 200000,
"cost": {
"amount": 0.045,
"currency": "USD"
}
})
);
let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
"sessionUpdate": "usage_update",
"used": 53000,
"size": 200000,
"cost": null
}))
.unwrap() else {
panic!("expected usage update");
};
assert_eq!(update.cost, None);
}
#[test]
fn session_update_preserves_unknown_variant() {
use serde_json::json;
let update: SessionUpdate = serde_json::from_value(json!({
"sessionUpdate": "_status_badge",
"label": "Indexing",
"progress": 0.5
}))
.unwrap();
let SessionUpdate::Other(unknown) = update else {
panic!("expected unknown session update");
};
assert_eq!(unknown.session_update, "_status_badge");
assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
assert_eq!(
serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
json!({
"sessionUpdate": "_status_badge",
"label": "Indexing",
"progress": 0.5
})
);
}
#[test]
fn test_plan_update_serialization() {
use serde_json::json;
let plan_update =
SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
"plan-1",
vec![crate::v2::PlanEntry::new(
"Step 1",
crate::v2::PlanEntryPriority::High,
crate::v2::PlanEntryStatus::Pending,
)],
)));
assert_eq!(
serde_json::to_value(plan_update).unwrap(),
json!({
"sessionUpdate": "plan_update",
"plan": {
"type": "items",
"id": "plan-1",
"entries": [
{
"content": "Step 1",
"priority": "high",
"status": "pending"
}
]
}
})
);
}
#[cfg(feature = "unstable_plan_operations")]
#[test]
fn test_plan_removed_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
json!({
"sessionUpdate": "plan_removed",
"id": "plan-1"
})
);
}
#[test]
fn available_command_input_preserves_unknown_typed_variant() {
use serde_json::json;
let input: AvailableCommandInput = serde_json::from_value(json!({
"type": "_choices",
"hint": "Pick one",
"options": ["fast", "careful"]
}))
.unwrap();
let AvailableCommandInput::Other(unknown) = input else {
panic!("expected unknown command input");
};
assert_eq!(unknown.type_, "_choices");
assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
assert_eq!(
unknown.fields.get("options"),
Some(&json!(["fast", "careful"]))
);
assert_eq!(
serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
json!({
"type": "_choices",
"hint": "Pick one",
"options": ["fast", "careful"]
})
);
}
#[test]
fn available_command_input_unknown_does_not_hide_malformed_unstructured_variant() {
use serde_json::json;
assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
assert!(
serde_json::from_value::<AvailableCommandInput>(json!({
"type": 1,
"hint": "Pick one"
}))
.is_err()
);
}
#[cfg(feature = "unstable_nes")]
#[test]
fn test_client_capabilities_position_encodings_serialization() {
use serde_json::json;
let capabilities = ClientCapabilities::new().position_encodings(vec![
PositionEncodingKind::Utf32,
PositionEncodingKind::Utf16,
]);
let json = serde_json::to_value(&capabilities).unwrap();
assert_eq!(json["positionEncodings"], json!(["utf-32", "utf-16"]));
}
#[cfg(feature = "unstable_mcp_over_acp")]
#[test]
fn test_agent_mcp_request_method_names() {
use serde_json::json;
let params: serde_json::Map<String, serde_json::Value> =
[("cursor".to_string(), json!("abc"))].into_iter().collect();
assert_eq!(CLIENT_METHOD_NAMES.mcp_connect, "mcp/connect");
assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
assert_eq!(CLIENT_METHOD_NAMES.mcp_disconnect, "mcp/disconnect");
assert_eq!(
AgentRequest::ConnectMcpRequest(ConnectMcpRequest::new("server-1")).method(),
"mcp/connect"
);
assert_eq!(
AgentRequest::MessageMcpRequest(MessageMcpRequest::new("conn-1", "tools/list"))
.method(),
"mcp/message"
);
assert_eq!(
AgentRequest::DisconnectMcpRequest(DisconnectMcpRequest::new("conn-1")).method(),
"mcp/disconnect"
);
assert_eq!(
AgentNotification::MessageMcpNotification(MessageMcpNotification::new(
"conn-1",
"notifications/progress"
))
.method(),
"mcp/message"
);
assert_eq!(
serde_json::to_value(ConnectMcpRequest::new("server-1")).unwrap(),
json!({ "acpId": "server-1" })
);
assert_eq!(
serde_json::to_value(ConnectMcpResponse::new("conn-1")).unwrap(),
json!({ "connectionId": "conn-1" })
);
assert_eq!(
serde_json::to_value(MessageMcpRequest::new("conn-1", "tools/list").params(params))
.unwrap(),
json!({
"connectionId": "conn-1",
"method": "tools/list",
"params": { "cursor": "abc" }
})
);
assert_eq!(
serde_json::to_value(DisconnectMcpRequest::new("conn-1")).unwrap(),
json!({ "connectionId": "conn-1" })
);
assert_eq!(
serde_json::to_value(MessageMcpNotification::new(
"conn-1",
"notifications/progress"
))
.unwrap(),
json!({
"connectionId": "conn-1",
"method": "notifications/progress"
})
);
let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
"connectionId": "conn-1",
"method": "tools/list",
"params": null
}))
.unwrap();
assert_eq!(request_with_null_params.params, None);
}
#[cfg(feature = "unstable_auth_methods")]
#[test]
fn test_auth_capabilities_serialize_terminal_support_as_object() {
use serde_json::json;
let capabilities = AuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
assert_eq!(
serde_json::to_value(&capabilities).unwrap(),
json!({
"terminal": {}
})
);
let deserialized: AuthCapabilities = serde_json::from_value(json!({
"terminal": false
}))
.unwrap();
assert!(deserialized.terminal.is_none());
}
}