use std::{collections::BTreeMap, sync::Arc};
use derive_more::{Display, From};
#[cfg(feature = "schemars")]
use schemars::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_end_turn_token_usage")]
use super::Usage;
use super::{
AbsolutePath, ContentBlock, ExtNotification, ExtRequest, ExtResponse, Meta, PlanUpdate,
SessionConfigOption, SessionId, StopReason, TerminalId, TerminalOutputChunk, TerminalUpdate,
ToolCallContentChunk, ToolCallId, ToolCallUpdate,
};
use super::{
CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse,
ElicitationCapabilities,
};
use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener};
#[cfg(feature = "unstable_mcp_over_acp")]
use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse};
#[cfg(feature = "unstable_nes")]
use super::{ClientNesCapabilities, PositionEncodingKind};
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION)))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UpdateSessionNotification {
pub session_id: SessionId,
pub update: SessionUpdate,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl UpdateSessionNotification {
#[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
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
#[non_exhaustive]
pub enum SessionUpdate {
UserMessageChunk(ContentChunk),
UserMessage(UserMessage),
AgentMessageChunk(ContentChunk),
AgentMessage(AgentMessage),
AgentThoughtChunk(ContentChunk),
AgentThought(AgentThought),
StateUpdate(StateUpdate),
ToolCallContentChunk(ToolCallContentChunk),
ToolCallUpdate(ToolCallUpdate),
TerminalUpdate(TerminalUpdate),
TerminalOutputChunk(TerminalOutputChunk),
PlanUpdate(PlanUpdate),
#[cfg(feature = "unstable_plan_operations")]
PlanRemoved(PlanRemoved),
AvailableCommandsUpdate(AvailableCommandsUpdate),
ConfigOptionUpdate(ConfigOptionUpdate),
SessionInfoUpdate(SessionInfoUpdate),
UsageUpdate(UsageUpdate),
#[cfg(feature = "unstable_session_notices")]
Notice(Notice),
#[cfg(feature = "unstable_session_compaction")]
CompactionUpdate(CompactionUpdate),
#[cfg(feature = "unstable_session_compaction")]
CompactionSummaryChunk(CompactionSummaryChunk),
#[cfg(feature = "unstable_subagents")]
SubagentUpdate(SubagentUpdate),
#[cfg(feature = "unstable_subagents")]
SessionMessage(SessionMessage),
#[cfg(feature = "unstable_subagents")]
SessionMessageChunk(SessionMessageChunk),
#[serde(untagged)]
Other(OtherSessionUpdate),
}
#[cfg(feature = "unstable_subagents")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SessionMessageChunk {
pub message_id: MessageId,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub sender_session_id: Option<SessionId>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub recipient_session_id: Option<SessionId>,
pub content: ContentBlock,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_subagents")]
impl SessionMessageChunk {
#[must_use]
pub fn new(message_id: impl Into<MessageId>, content: ContentBlock) -> Self {
Self {
message_id: message_id.into(),
sender_session_id: None,
recipient_session_id: None,
content,
meta: None,
}
}
#[must_use]
pub fn sender_session_id(mut self, sender_session_id: impl IntoOption<SessionId>) -> Self {
self.sender_session_id = sender_session_id.into_option();
self
}
#[must_use]
pub fn recipient_session_id(
mut self,
recipient_session_id: impl IntoOption<SessionId>,
) -> Self {
self.recipient_session_id = recipient_session_id.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_subagents")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SessionMessage {
pub message_id: MessageId,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub sender_session_id: Option<SessionId>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub recipient_session_id: Option<SessionId>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
default,
rename = "_meta",
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<Meta>,
}
#[cfg(feature = "unstable_subagents")]
impl SessionMessage {
#[must_use]
pub fn new(message_id: impl Into<MessageId>) -> Self {
Self {
message_id: message_id.into(),
sender_session_id: None,
recipient_session_id: None,
content: MaybeUndefined::Undefined,
meta: MaybeUndefined::Undefined,
}
}
#[must_use]
pub fn sender_session_id(mut self, sender_session_id: impl IntoOption<SessionId>) -> Self {
self.sender_session_id = sender_session_id.into_option();
self
}
#[must_use]
pub fn recipient_session_id(
mut self,
recipient_session_id: impl IntoOption<SessionId>,
) -> Self {
self.recipient_session_id = recipient_session_id.into_option();
self
}
#[must_use]
pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
self.content = content.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
#[cfg(all(test, feature = "unstable_subagents"))]
mod session_message_tests {
use super::*;
use serde_json::{Value, json};
#[test]
fn transcript_envelopes_and_multimodal_content() {
for (transcript, sender, recipient, id) in [
("parent", "parent", "child", "sent-1"),
("child", "parent", "child", "received-9"),
("child", "child", "parent", "sent-2"),
] {
let wire = json!({"sessionId": transcript, "update": {
"sessionUpdate": "session_message", "messageId": id,
"senderSessionId": sender, "recipientSessionId": recipient, "content": [
{"type": "text", "text": "hello"},
{"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"}
]
}});
let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
}
}
#[test]
fn chunks_and_upserts_share_transcript_local_identity() {
for (transcript, id) in [("parent", "sent-1"), ("child", "received-9")] {
let wire = json!({"sessionId": transcript, "update": {
"sessionUpdate": "session_message_chunk", "messageId": id,
"senderSessionId": "parent", "recipientSessionId": "child",
"content": {"type": "text", "text": "hello"}
}});
let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
let upsert = SessionMessage::new(id)
.sender_session_id(SessionId::new("parent"))
.recipient_session_id(SessionId::new("child"))
.content(vec![]);
let chunk = SessionMessageChunk::new(
id,
ContentBlock::Text(crate::v2::TextContent::new("hello")),
)
.sender_session_id(SessionId::new("parent"))
.recipient_session_id(SessionId::new("child"));
assert_eq!(chunk.message_id, upsert.message_id);
assert_eq!(chunk.sender_session_id, upsert.sender_session_id);
assert_eq!(chunk.recipient_session_id, upsert.recipient_session_id);
assert_eq!(
serde_json::to_value(SessionUpdate::SessionMessageChunk(chunk)).unwrap(),
wire["update"]
);
assert_eq!(serde_json::to_value(upsert).unwrap()["content"], json!([]));
}
}
#[test]
fn required_ids_and_chunk_content_with_upsert_patch_semantics() {
for (kind, content) in [
("session_message", json!([])),
(
"session_message_chunk",
json!({"type": "text", "text": "hello"}),
),
] {
let base = json!({"sessionUpdate": kind, "messageId": "m1",
"senderSessionId": "parent", "recipientSessionId": "child", "content": content});
{
let key = "messageId";
let mut missing = base.clone();
missing.as_object_mut().unwrap().remove(key);
assert!(
serde_json::from_value::<SessionUpdate>(missing).is_err(),
"{kind} {key}"
);
let mut null = base.clone();
null[key] = Value::Null;
assert!(
serde_json::from_value::<SessionUpdate>(null).is_err(),
"{kind} {key}"
);
let mut non_string = base.clone();
non_string[key] = json!(42);
assert!(
serde_json::from_value::<SessionUpdate>(non_string).is_err(),
"{kind} {key}"
);
}
if kind == "session_message_chunk" {
for content in [None, Some(Value::Null), Some(json!([]))] {
let mut invalid = base.clone();
invalid.as_object_mut().unwrap().remove("content");
if let Some(content) = content {
invalid["content"] = content;
}
assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
}
} else {
for content in [None, Some(Value::Null), Some(json!([])), Some(json!({}))] {
let mut wire = base.clone();
wire.as_object_mut().unwrap().remove("content");
if let Some(content) = content {
wire["content"] = content;
}
let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
let encoded = serde_json::to_value(decoded).unwrap();
if wire["content"].is_object() {
wire.as_object_mut().unwrap().remove("content");
}
assert_eq!(encoded, wire);
}
}
for meta in [None, Some(Value::Null), Some(json!({"tag": "value"}))] {
let mut wire = base.clone();
if let Some(meta) = meta {
wire["_meta"] = meta;
}
let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
let encoded = serde_json::to_value(decoded).unwrap();
if kind == "session_message" {
assert_eq!(encoded, wire);
} else if wire["_meta"].is_null() {
assert_eq!(encoded, base);
} else {
assert_eq!(encoded, wire);
}
}
}
let metadata_only = json!({"sessionUpdate": "session_message",
"messageId": "m1", "senderSessionId": "parent",
"recipientSessionId": "child", "_meta": {"tag": "value"}});
let decoded: SessionUpdate = serde_json::from_value(metadata_only.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), metadata_only);
let cleared = SessionMessage::new("m1")
.sender_session_id(SessionId::new("parent"))
.recipient_session_id(SessionId::new("child"))
.content(None)
.meta(None);
assert_eq!(
serde_json::to_value(cleared).unwrap(),
json!({"messageId": "m1", "senderSessionId": "parent",
"recipientSessionId": "child", "content": null, "_meta": null})
);
}
#[cfg(feature = "schemars")]
#[test]
fn schemas_require_ids_and_chunk_content_but_not_upsert_content() {
let upsert = serde_json::to_value(schemars::schema_for!(SessionMessage)).unwrap();
let required = upsert["required"].as_array().unwrap();
assert_eq!(required, &vec![json!("messageId")]);
let chunk = serde_json::to_value(schemars::schema_for!(SessionMessageChunk)).unwrap();
let required = chunk["required"].as_array().unwrap();
assert_eq!(required.len(), 2);
assert!(required.contains(&json!("messageId")));
assert!(required.contains(&json!("content")));
}
#[test]
fn endpoints_can_arrive_late_or_be_omitted_from_later_events() {
let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
let minimal = SessionMessage::new("m1");
let first = SessionMessageChunk::new("m1", block.clone());
assert_eq!(
serde_json::to_value(&minimal).unwrap(),
json!({"messageId": "m1"})
);
assert_eq!(
serde_json::to_value(&first).unwrap(),
json!({"messageId": "m1", "content": {"type": "text", "text": "hello"}})
);
let enriched = SessionMessage::new("m1")
.sender_session_id(SessionId::new("parent"))
.recipient_session_id(SessionId::new("child"));
assert_eq!(enriched.sender_session_id, Some(SessionId::new("parent")));
assert_eq!(enriched.recipient_session_id, Some(SessionId::new("child")));
let later =
SessionMessageChunk::new("m1", block).sender_session_id(SessionId::new("parent"));
assert_eq!(later.sender_session_id, Some(SessionId::new("parent")));
assert_eq!(later.recipient_session_id, None);
for update in [
SessionUpdate::SessionMessage(minimal),
SessionUpdate::SessionMessageChunk(first),
SessionUpdate::SessionMessage(enriched),
SessionUpdate::SessionMessageChunk(later),
] {
let wire = serde_json::to_value(&update).unwrap();
let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
}
}
#[test]
fn invalid_or_null_endpoints_are_absent_but_message_id_is_required() {
for (kind, content) in [
("session_message", None),
(
"session_message_chunk",
Some(json!({"type": "text", "text": "hello"})),
),
] {
let mut base = json!({"sessionUpdate": kind, "messageId": "m1",
"senderSessionId": null, "recipientSessionId": 42});
if let Some(content) = content {
base["content"] = content;
}
let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap();
let encoded = serde_json::to_value(decoded).unwrap();
base.as_object_mut().unwrap().remove("senderSessionId");
base.as_object_mut().unwrap().remove("recipientSessionId");
assert_eq!(encoded, base);
for bad in [Value::Null, json!(42)] {
let mut invalid = base.clone();
invalid["messageId"] = bad;
assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
}
}
}
}
#[cfg(all(test, not(feature = "unstable_subagents")))]
mod disabled_session_message_tests {
use super::*;
use serde_json::json;
#[test]
fn preserves_both_unknown_message_discriminators() {
for (kind, content) in [
("session_message", json!([])),
(
"session_message_chunk",
json!({"type": "text", "text": "hello"}),
),
] {
let wire = json!({"sessionUpdate": kind, "messageId": "m1",
"senderSessionId": "parent", "recipientSessionId": "child", "content": content});
let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
assert!(matches!(decoded, SessionUpdate::Other(_)));
assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
}
}
}
#[cfg(feature = "unstable_session_notices")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum NoticeSeverity {
Info,
Warning,
Error,
#[serde(untagged)]
Other(String),
}
#[cfg(feature = "unstable_session_notices")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Notice {
pub severity: NoticeSeverity,
#[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
pub title: String,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub description: Option<String>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_session_notices")]
impl Notice {
#[must_use]
pub fn new(severity: NoticeSeverity, title: impl Into<String>) -> Self {
Self {
severity,
title: title.into(),
description: None,
meta: None,
}
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_session_compaction")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(forward)]
#[non_exhaustive]
pub struct CompactionId(pub Arc<str>);
#[cfg(feature = "unstable_session_compaction")]
impl CompactionId {
#[must_use]
pub fn new(id: impl Into<Self>) -> Self {
id.into()
}
}
#[cfg(feature = "unstable_session_compaction")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CompactionStatus {
InProgress,
Completed,
Failed,
Cancelled,
#[serde(untagged)]
Other(String),
}
#[cfg(feature = "unstable_session_compaction")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CompactionUpdate {
pub compaction_id: CompactionId,
pub status: CompactionStatus,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub summary: MaybeUndefined<Vec<ContentBlock>>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub error: MaybeUndefined<String>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
rename = "_meta",
default,
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<Meta>,
}
#[cfg(feature = "unstable_session_compaction")]
impl CompactionUpdate {
#[must_use]
pub fn new(compaction_id: impl Into<CompactionId>, status: CompactionStatus) -> Self {
Self {
compaction_id: compaction_id.into(),
status,
summary: MaybeUndefined::Undefined,
error: MaybeUndefined::Undefined,
meta: MaybeUndefined::Undefined,
}
}
#[must_use]
pub fn summary(mut self, summary: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
self.summary = summary.into_maybe_undefined();
self
}
#[must_use]
pub fn error(mut self, error: impl IntoMaybeUndefined<String>) -> Self {
self.error = error.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
#[cfg(feature = "unstable_session_compaction")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CompactionSummaryChunk {
pub compaction_id: CompactionId,
pub content: ContentBlock,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_session_compaction")]
impl CompactionSummaryChunk {
#[must_use]
pub fn new(compaction_id: impl Into<CompactionId>, content: ContentBlock) -> Self {
Self {
compaction_id: compaction_id.into(),
content,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_subagents")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SubagentUpdate {
pub session_id: SessionId,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub title: MaybeUndefined<String>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub description: MaybeUndefined<String>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub capabilities: MaybeUndefined<SubagentSessionCapabilities>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub state: MaybeUndefined<StateUpdate>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
rename = "_meta",
default,
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<Meta>,
}
#[cfg(feature = "unstable_subagents")]
impl SubagentUpdate {
#[must_use]
pub fn new(session_id: impl Into<SessionId>) -> Self {
Self {
session_id: session_id.into(),
title: MaybeUndefined::Undefined,
description: MaybeUndefined::Undefined,
capabilities: MaybeUndefined::Undefined,
state: MaybeUndefined::Undefined,
meta: MaybeUndefined::Undefined,
}
}
#[must_use]
pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
self.title = title.into_maybe_undefined();
self
}
#[must_use]
pub fn description(mut self, description: impl IntoMaybeUndefined<String>) -> Self {
self.description = description.into_maybe_undefined();
self
}
#[must_use]
pub fn capabilities(
mut self,
capabilities: impl IntoMaybeUndefined<SubagentSessionCapabilities>,
) -> Self {
self.capabilities = capabilities.into_maybe_undefined();
self
}
#[must_use]
pub fn state(mut self, state: impl IntoMaybeUndefined<StateUpdate>) -> Self {
self.state = state.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
#[cfg(feature = "unstable_subagents")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SubagentSessionCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub cancel: Option<SessionCancelCapabilities>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_subagents")]
impl SubagentSessionCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn cancel(mut self, cancel: impl IntoOption<SessionCancelCapabilities>) -> Self {
self.cancel = cancel.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_subagents")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct SessionCancelCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_subagents")]
impl SessionCancelCapabilities {
#[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
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[cfg_attr(feature = "schemars", 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 {
#[cfg(feature = "unstable_session_notices")]
if session_update == "notice" {
return true;
}
#[cfg(feature = "unstable_session_compaction")]
if matches!(
session_update,
"compaction_update" | "compaction_summary_chunk"
) {
return true;
}
#[cfg(feature = "unstable_plan_operations")]
if session_update == "plan_removed" {
return true;
}
#[cfg(feature = "unstable_subagents")]
if matches!(
session_update,
"subagent_update" | "session_message" | "session_message_chunk"
) {
return true;
}
matches!(
session_update,
"user_message_chunk"
| "user_message"
| "agent_message_chunk"
| "agent_message"
| "agent_thought_chunk"
| "agent_thought"
| "state_update"
| "tool_call_content_chunk"
| "tool_call_update"
| "terminal_update"
| "terminal_output_chunk"
| "plan_update"
| "available_commands_update"
| "config_option_update"
| "session_info_update"
| "usage_update"
)
}
#[cfg(feature = "schemars")]
fn other_session_update_schema(schema: &mut Schema) {
super::schema_util::reject_known_string_discriminators(
schema,
"sessionUpdate",
&[
"user_message_chunk",
"user_message",
"agent_message_chunk",
"agent_message",
"agent_thought_chunk",
"agent_thought",
"state_update",
"tool_call_content_chunk",
"tool_call_update",
"terminal_update",
"terminal_output_chunk",
"plan_update",
"available_commands_update",
"config_option_update",
"session_info_update",
#[cfg(feature = "unstable_plan_operations")]
"plan_removed",
"usage_update",
#[cfg(feature = "unstable_session_notices")]
"notice",
#[cfg(feature = "unstable_session_compaction")]
"compaction_update",
#[cfg(feature = "unstable_session_compaction")]
"compaction_summary_chunk",
#[cfg(feature = "unstable_subagents")]
"subagent_update",
#[cfg(feature = "unstable_subagents")]
"session_message",
#[cfg(feature = "unstable_subagents")]
"session_message_chunk",
],
);
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ConfigOptionUpdate {
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
pub config_options: Vec<SessionConfigOption>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SessionInfoUpdate {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub title: MaybeUndefined<String>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub updated_at: MaybeUndefined<String>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
rename = "_meta",
default,
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<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 IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UsageUpdate {
pub used: u64,
pub size: u64,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub cost: Option<Cost>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "state", rename_all = "snake_case")]
#[non_exhaustive]
pub enum StateUpdate {
Running(RunningStateUpdate),
Idle(IdleStateUpdate),
RequiresAction(RequiresActionStateUpdate),
#[cfg(feature = "unstable_subagents")]
Unknown(UnknownStateUpdate),
#[serde(untagged)]
Other(OtherStateUpdate),
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RunningStateUpdate {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl RunningStateUpdate {
#[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
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct IdleStateUpdate {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub stop_reason: Option<StopReason>,
#[cfg(feature = "unstable_end_turn_token_usage")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub usage: Option<Usage>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl IdleStateUpdate {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
self.stop_reason = stop_reason.into_option();
self
}
#[cfg(feature = "unstable_end_turn_token_usage")]
#[must_use]
pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
self.usage = usage.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RequiresActionStateUpdate {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl RequiresActionStateUpdate {
#[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
}
}
#[cfg(feature = "unstable_subagents")]
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UnknownStateUpdate {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_subagents")]
impl UnknownStateUpdate {
#[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
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OtherStateUpdate {
#[serde(rename = "state")]
pub state: String,
#[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>,
}
impl OtherStateUpdate {
#[must_use]
pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
fields.remove("state");
Self {
state: state.into(),
fields,
}
}
}
impl<'de> Deserialize<'de> for OtherStateUpdate {
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 state = fields
.remove("state")
.ok_or_else(|| serde::de::Error::missing_field("state"))?;
let serde_json::Value::String(state) = state else {
return Err(serde::de::Error::custom("`state` must be a string"));
};
if is_known_state_update(&state) {
return Err(serde::de::Error::custom(format!(
"known state update `{state}` did not match its schema"
)));
}
Ok(Self { state, fields })
}
}
const KNOWN_STATE_UPDATE_STATES: &[&str] = &[
"running",
"idle",
"requires_action",
#[cfg(feature = "unstable_subagents")]
"unknown",
];
fn is_known_state_update(state: &str) -> bool {
KNOWN_STATE_UPDATE_STATES.contains(&state)
}
#[cfg(feature = "schemars")]
fn other_state_update_schema(schema: &mut Schema) {
super::schema_util::reject_known_string_discriminators(
schema,
"state",
KNOWN_STATE_UPDATE_STATES,
);
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Cost {
pub amount: f64,
#[cfg_attr(feature = "schemars", schemars(pattern(r"^[A-Z]{3}$")))]
pub currency: String,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl Cost {
#[must_use]
pub fn new(amount: f64, currency: impl Into<String>) -> Self {
Self {
amount,
currency: currency.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]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ContentChunk {
pub message_id: MessageId,
pub content: ContentBlock,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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 meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UserMessage {
pub message_id: MessageId,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
rename = "_meta",
default,
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<Meta>,
}
impl UserMessage {
#[must_use]
pub fn new(message_id: impl Into<MessageId>) -> Self {
Self {
message_id: message_id.into(),
content: MaybeUndefined::Undefined,
meta: MaybeUndefined::Undefined,
}
}
#[must_use]
pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
self.content = content.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentMessage {
pub message_id: MessageId,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
rename = "_meta",
default,
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<Meta>,
}
impl AgentMessage {
#[must_use]
pub fn new(message_id: impl Into<MessageId>) -> Self {
Self {
message_id: message_id.into(),
content: MaybeUndefined::Undefined,
meta: MaybeUndefined::Undefined,
}
}
#[must_use]
pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
self.content = content.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentThought {
pub message_id: MessageId,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
rename = "_meta",
default,
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<Meta>,
}
impl AgentThought {
#[must_use]
pub fn new(message_id: impl Into<MessageId>) -> Self {
Self {
message_id: message_id.into(),
content: MaybeUndefined::Undefined,
meta: MaybeUndefined::Undefined,
}
}
#[must_use]
pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
self.content = content.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(forward)]
#[non_exhaustive]
pub struct MessageId(pub Arc<str>);
impl MessageId {
#[must_use]
pub fn new(id: impl Into<Self>) -> Self {
id.into()
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AvailableCommandsUpdate {
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
pub available_commands: Vec<AvailableCommand>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AvailableCommand {
pub name: String,
pub description: String,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub input: Option<AvailableCommandInput>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum AvailableCommandInput {
#[serde(rename = "text")]
Text(TextCommandInput),
#[serde(untagged)]
Other(OtherAvailableCommandInput),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
#[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"));
};
if is_known_available_command_input_type(&type_) {
return Err(serde::de::Error::custom(format!(
"known available command input type `{type_}` did not match its schema"
)));
}
Ok(Self { type_, fields })
}
}
const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
fn is_known_available_command_input_type(type_: &str) -> bool {
KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
}
#[cfg(feature = "schemars")]
fn other_available_command_input_schema(schema: &mut Schema) {
super::schema_util::reject_known_string_discriminators(
schema,
"type",
KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
);
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TextCommandInput {
pub hint: String,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl TextCommandInput {
#[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
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", 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 title: String,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub subject: Option<RequestPermissionSubject>,
#[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
pub options: Vec<PermissionOption>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl RequestPermissionRequest {
#[must_use]
pub fn new(
session_id: impl Into<SessionId>,
title: impl Into<String>,
options: Vec<PermissionOption>,
) -> Self {
Self {
session_id: session_id.into(),
title: title.into(),
description: None,
subject: None,
options,
meta: None,
}
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
self.subject = subject.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum RequestPermissionSubject {
ToolCall(Box<ToolCallPermissionSubject>),
Command(CommandPermissionSubject),
#[serde(untagged)]
Other(OtherRequestPermissionSubject),
}
impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
fn from(subject: ToolCallPermissionSubject) -> Self {
Self::ToolCall(Box::new(subject))
}
}
impl From<ToolCallUpdate> for RequestPermissionSubject {
fn from(tool_call: ToolCallUpdate) -> Self {
ToolCallPermissionSubject::new(tool_call).into()
}
}
impl From<CommandPermissionSubject> for RequestPermissionSubject {
fn from(subject: CommandPermissionSubject) -> Self {
Self::Command(subject)
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ToolCallPermissionSubject {
pub tool_call: ToolCallUpdate,
}
impl ToolCallPermissionSubject {
#[must_use]
pub fn new(tool_call: ToolCallUpdate) -> Self {
Self { tool_call }
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CommandPermissionSubject {
pub command: String,
pub cwd: AbsolutePath,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub tool_call_id: Option<ToolCallId>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub terminal_id: Option<TerminalId>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl CommandPermissionSubject {
#[must_use]
pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
Self {
command: command.into(),
cwd: cwd.into(),
tool_call_id: None,
terminal_id: None,
meta: None,
}
}
#[must_use]
pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
self.tool_call_id = tool_call_id.into_option();
self
}
#[must_use]
pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
self.terminal_id = terminal_id.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OtherRequestPermissionSubject {
#[serde(rename = "type")]
pub type_: String,
#[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>,
}
impl OtherRequestPermissionSubject {
#[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 OtherRequestPermissionSubject {
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"));
};
if is_known_request_permission_subject_type(&type_) {
return Err(serde::de::Error::custom(format!(
"known request permission subject `{type_}` did not match its schema"
)));
}
Ok(Self { type_, fields })
}
}
fn is_known_request_permission_subject_type(type_: &str) -> bool {
matches!(type_, "tool_call" | "command")
}
#[cfg(feature = "schemars")]
fn other_request_permission_subject_schema(schema: &mut Schema) {
super::schema_util::reject_known_string_discriminators(
schema,
"type",
&["tool_call", "command"],
);
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PermissionOption {
pub option_id: PermissionOptionId,
pub name: String,
pub kind: PermissionOptionKind,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(forward)]
#[non_exhaustive]
pub struct PermissionOptionId(pub Arc<str>);
impl PermissionOptionId {
#[must_use]
pub fn new(id: impl Into<Self>) -> Self {
id.into()
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PermissionOptionKind {
AllowOnce,
AllowAlways,
RejectOnce,
RejectAlways,
#[serde(untagged)]
Other(String),
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", 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_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "outcome", rename_all = "snake_case")]
#[non_exhaustive]
pub enum RequestPermissionOutcome {
Cancelled,
#[serde(rename_all = "camelCase")]
Selected(SelectedPermissionOutcome),
#[serde(untagged)]
Other(OtherRequestPermissionOutcome),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OtherRequestPermissionOutcome {
pub outcome: String,
#[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>,
}
impl OtherRequestPermissionOutcome {
#[must_use]
pub fn new(
outcome: impl Into<String>,
mut fields: BTreeMap<String, serde_json::Value>,
) -> Self {
fields.remove("outcome");
Self {
outcome: outcome.into(),
fields,
}
}
}
impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
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 outcome = fields
.remove("outcome")
.ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
let serde_json::Value::String(outcome) = outcome else {
return Err(serde::de::Error::custom("`outcome` must be a string"));
};
if is_known_request_permission_outcome(&outcome) {
return Err(serde::de::Error::custom(format!(
"known request permission outcome `{outcome}` did not match its schema"
)));
}
Ok(Self { outcome, fields })
}
}
fn is_known_request_permission_outcome(outcome: &str) -> bool {
matches!(outcome, "cancelled" | "selected")
}
#[cfg(feature = "schemars")]
fn other_request_permission_outcome_schema(schema: &mut Schema) {
super::schema_util::reject_known_string_discriminators(
schema,
"outcome",
&["cancelled", "selected"],
);
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SelectedPermissionOutcome {
pub option_id: PermissionOptionId,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[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]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ClientCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub auth: Option<AuthCapabilities>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub elicitation: Option<ElicitationCapabilities>,
#[cfg(feature = "unstable_nes")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", 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>>")]
#[cfg_attr(feature = "schemars", 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_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl ClientCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn auth(mut self, auth: impl IntoOption<AuthCapabilities>) -> Self {
self.auth = auth.into_option();
self
}
#[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
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AuthCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub terminal: Option<TerminalAuthCapabilities>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
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
}
}
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct TerminalAuthCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
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_message: &'static str,
pub elicitation_create: &'static str,
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_message: MCP_MESSAGE_METHOD_NAME,
elicitation_create: ELICITATION_CREATE_METHOD_NAME,
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";
pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[non_exhaustive]
pub enum AgentRequest {
RequestPermissionRequest(Box<RequestPermissionRequest>),
CreateElicitationRequest(Box<CreateElicitationRequest>),
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpRequest(Box<MessageMcpRequest>),
ExtMethodRequest(Box<ExtRequest>),
}
impl AgentRequest {
#[must_use]
pub fn method(&self) -> &str {
match self {
Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
#[cfg(feature = "unstable_mcp_over_acp")]
Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
Self::ExtMethodRequest(ext_request) => &ext_request.method,
}
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[non_exhaustive]
pub enum ClientResponse {
RequestPermissionResponse(Box<RequestPermissionResponse>),
CreateElicitationResponse(Box<CreateElicitationResponse>),
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpResponse(Box<MessageMcpResponse>),
ExtMethodResponse(Box<ExtResponse>),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[non_exhaustive]
pub enum AgentNotification {
UpdateSessionNotification(Box<UpdateSessionNotification>),
CompleteElicitationNotification(Box<CompleteElicitationNotification>),
ExtNotification(Box<ExtNotification>),
}
impl AgentNotification {
#[must_use]
pub fn method(&self) -> &str {
match self {
Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
Self::ExtNotification(ext_notification) => &ext_notification.method,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "unstable_subagents")]
#[test]
fn subagent_update_serializes_as_upsert() {
use serde_json::json;
let announced =
SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
));
let wire = json!({
"sessionUpdate": "subagent_update",
"sessionId": "sess_child_1",
"capabilities": { "cancel": {} }
});
assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
assert_eq!(
serde_json::from_value::<SessionUpdate>(wire).unwrap(),
announced
);
let minimal = SubagentUpdate::new("sess_child_1");
assert_eq!(
serde_json::to_value(&minimal).unwrap(),
json!({
"sessionId": "sess_child_1"
})
);
assert!(minimal.capabilities.is_undefined());
assert!(minimal.state.is_undefined());
assert!(minimal.meta.is_undefined());
let cleared_wire = json!({
"sessionId": "sess_child_1",
"capabilities": null,
"state": null,
"_meta": null
});
let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
assert!(cleared.capabilities.is_null());
assert!(cleared.state.is_null());
assert!(cleared.meta.is_null());
assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
let revoked: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "sess_child_1",
"capabilities": {}
}))
.unwrap();
assert_eq!(
revoked.capabilities,
MaybeUndefined::Value(SubagentSessionCapabilities::new())
);
assert!(
matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
);
assert!(
serde_json::from_value::<SessionUpdate>(json!({
"sessionUpdate": "subagent_update"
}))
.is_err()
);
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn subagent_cancel_capability_is_optional_object() {
use serde_json::json;
for wire in [
json!({}),
json!({"cancel": null}),
json!({"cancel": true}),
json!({"cancel": false}),
] {
let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
assert!(child.cancel.is_none());
assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
}
let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
assert_eq!(
serde_json::to_value(&enabled).unwrap(),
json!({"cancel": {}})
);
assert_eq!(
serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
enabled
);
let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
let with_meta =
SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
assert_eq!(
serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
with_meta
);
assert!(enabled.cancel.is_some());
assert!(enabled.cancel(None).cancel.is_none());
let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
assert_eq!(
serde_json::to_value(removed).unwrap(),
json!({"sessionId": "child", "capabilities": {}})
);
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn subagent_display_metadata_uses_patch_semantics() {
use serde_json::json;
let update = SubagentUpdate::new("child")
.title("Test investigator".to_string())
.description("Investigates platform-specific test failures.".to_string());
let wire = json!({
"sessionId": "child",
"title": "Test investigator",
"description": "Investigates platform-specific test failures."
});
assert_eq!(serde_json::to_value(&update).unwrap(), wire);
assert_eq!(
serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
update
);
let minimal = SubagentUpdate::new("child");
assert!(minimal.title.is_undefined());
assert!(minimal.description.is_undefined());
assert_eq!(
serde_json::to_value(&minimal).unwrap(),
json!({"sessionId": "child"})
);
let cleared_wire = json!({
"sessionId": "child", "title": null, "description": null
});
let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
assert!(cleared.title.is_null());
assert!(cleared.description.is_null());
assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
let title_only: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "child", "title": "Updated title"
}))
.unwrap();
assert_eq!(
title_only.title,
MaybeUndefined::Value("Updated title".to_string())
);
assert!(title_only.description.is_undefined());
let malformed: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "child", "title": false, "description": false
}))
.unwrap();
assert_eq!(malformed, minimal);
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn subagent_notification_keeps_parent_and_child_ids_nested() {
use serde_json::json;
let notification = UpdateSessionNotification::new(
"parent",
SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
);
let wire = json!({
"sessionId": "parent",
"update": {
"sessionUpdate": "subagent_update",
"sessionId": "child"
}
});
assert_eq!(serde_json::to_value(¬ification).unwrap(), wire);
assert_eq!(
serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
notification
);
for malformed in [
json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
] {
assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
}
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn subagent_mirrors_normal_child_state_notifications() {
use serde_json::json;
let association = SubagentUpdate::new("sess_child");
for state in [
StateUpdate::Running(RunningStateUpdate::new()),
StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
StateUpdate::Unknown(UnknownStateUpdate::new()),
StateUpdate::Running(RunningStateUpdate::new()),
StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
StateUpdate::Running(RunningStateUpdate::new()),
StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
] {
let child_notification = UpdateSessionNotification::new(
association.session_id.clone(),
SessionUpdate::StateUpdate(state.clone()),
);
let parent_notification = UpdateSessionNotification::new(
"sess_parent",
SessionUpdate::SubagentUpdate(association.clone().state(state)),
);
let child_wire = serde_json::to_value(&child_notification).unwrap();
let parent_wire = serde_json::to_value(&parent_notification).unwrap();
assert_eq!(child_wire["sessionId"], json!("sess_child"));
assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
let mut child_snapshot = child_wire["update"].clone();
child_snapshot
.as_object_mut()
.unwrap()
.remove("sessionUpdate");
assert_eq!(parent_wire["update"]["state"], child_snapshot);
assert_eq!(
serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
child_notification
);
assert_eq!(
serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
parent_notification
);
}
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn subagent_state_uses_whole_snapshot_patch_semantics() {
use serde_json::json;
let minimal = SubagentUpdate::new("child");
assert!(minimal.state.is_undefined());
assert_eq!(
serde_json::to_value(&minimal).unwrap(),
json!({"sessionId": "child"})
);
let cleared = minimal.clone().state(None::<StateUpdate>);
assert!(cleared.state.is_null());
assert_eq!(
serde_json::to_value(&cleared).unwrap(),
json!({"sessionId": "child", "state": null})
);
assert_eq!(
serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
.unwrap(),
cleared
);
for snapshot in [
json!({"state": "running", "_meta": {"source": "worker"}}),
json!({"state": "idle", "stopReason": "end_turn"}),
json!({"state": "requires_action"}),
json!({"state": "unknown"}),
json!({"state": "_custom", "detail": {"phase": 2}}),
] {
let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
let update = minimal.clone().state(state.clone());
let wire = json!({"sessionId": "child", "state": snapshot});
assert_eq!(serde_json::to_value(&update).unwrap(), wire);
assert_eq!(
serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
update
);
assert_eq!(update.state, MaybeUndefined::Value(state));
}
let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
));
let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
assert_eq!(
serde_json::to_value(running).unwrap(),
json!({"sessionId": "child", "state": {"state": "running"}})
);
for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
let parsed: SubagentUpdate =
serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
assert_eq!(parsed, minimal);
}
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn unknown_activity_is_a_known_state_update() {
use serde_json::json;
let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
UnknownStateUpdate::new().meta(
[("source".into(), json!("worker"))]
.into_iter()
.collect::<Meta>(),
),
));
let wire = json!({
"sessionUpdate": "state_update",
"state": "unknown",
"_meta": { "source": "worker" }
});
assert_eq!(serde_json::to_value(&update).unwrap(), wire);
assert_eq!(
serde_json::from_value::<SessionUpdate>(wire).unwrap(),
update
);
for meta in [json!(null), json!(false)] {
let state: StateUpdate = serde_json::from_value(json!({
"state": "unknown",
"_meta": meta
}))
.unwrap();
assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
assert_eq!(
serde_json::to_value(state).unwrap(),
json!({"state": "unknown"})
);
}
assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
}
#[cfg(not(feature = "unstable_subagents"))]
#[test]
fn unknown_activity_is_preserved_without_subagents_feature() {
let wire = serde_json::json!({
"sessionUpdate": "state_update",
"state": "unknown",
"_meta": { "source": "worker" }
});
let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
panic!("expected unrecognized state payload");
};
assert_eq!(state.state, "unknown");
assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
}
#[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
#[test]
fn subagent_schema_carries_optional_nullable_state_snapshot() {
use serde_json::json;
let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
let properties = schema["properties"].as_object().unwrap();
assert!(properties.contains_key("sessionId"));
assert!(properties.contains_key("title"));
assert!(properties.contains_key("description"));
assert!(properties.contains_key("capabilities"));
assert!(properties.contains_key("state"));
assert!(properties.contains_key("_meta"));
assert!(!properties.contains_key("name"));
assert!(!properties.contains_key("task"));
assert_eq!(schema["required"], json!(["sessionId"]));
assert_eq!(
properties["state"]["x-deserialize-default-on-error"],
json!(true)
);
let state_variants = properties["state"]["anyOf"].as_array().unwrap();
assert!(state_variants.contains(&json!({"type": "null"})));
assert!(
state_variants
.iter()
.any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
);
let child =
serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
let variants = child["properties"]["cancel"]["anyOf"]
.as_array()
.expect("cancel must allow the capability object or null");
assert!(variants.contains(&json!({"type": "null"})));
assert!(
variants
.iter()
.any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
);
assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
let cancel =
serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
assert_eq!(cancel["type"], "object");
assert!(
!child["required"]
.as_array()
.is_some_and(|required| required.contains(&json!("cancel")))
);
}
#[cfg(not(feature = "unstable_subagents"))]
#[test]
fn unsupported_subagent_update_is_preserved() {
let wire = serde_json::json!({
"sessionUpdate": "subagent_update",
"sessionId": "sess_child",
"capabilities": { "cancel": {} }
});
let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
assert!(matches!(&parsed, SessionUpdate::Other(_)));
assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
}
#[cfg(feature = "unstable_session_notices")]
#[test]
fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
use serde_json::json;
let mut meta = Meta::new();
meta.insert("source".into(), json!("fallback"));
let v2_notice = SessionUpdate::Notice(
Notice::new(NoticeSeverity::Error, "Provider degraded")
.description("Requests may take longer than usual.")
.meta(meta.clone()),
);
let expected = json!({
"sessionUpdate": "notice",
"severity": "error",
"title": "Provider degraded",
"description": "Requests may take longer than usual.",
"_meta": { "source": "fallback" }
});
assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
let v1_notice = crate::v1::SessionUpdate::Notice(
crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
.description("Requests may take longer than usual.")
.meta(meta),
);
assert_eq!(
serde_json::to_value(v2_notice).unwrap(),
serde_json::to_value(v1_notice).unwrap()
);
let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
"sessionUpdate": "notice",
"severity": "critical",
"title": "Provider degraded",
"description": null,
"_meta": null
}))
.unwrap() else {
panic!("expected notice");
};
assert_eq!(
notice.severity,
NoticeSeverity::Other("critical".to_string())
);
assert_eq!(notice.description, None);
assert_eq!(notice.meta, None);
assert_eq!(
serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
json!({
"sessionUpdate": "notice",
"severity": "critical",
"title": "Provider degraded"
})
);
}
#[cfg(feature = "unstable_session_notices")]
#[test]
fn malformed_known_notice_is_not_hidden_as_unknown() {
use serde_json::json;
for malformed in [
json!({
"sessionUpdate": "notice",
"severity": "warning"
}),
json!({
"sessionUpdate": "notice",
"severity": "warning",
"title": null
}),
json!({
"sessionUpdate": "notice",
"title": "MCP server unavailable"
}),
json!({
"sessionUpdate": "notice",
"severity": null,
"title": "MCP server unavailable"
}),
] {
assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
}
}
#[cfg(not(feature = "unstable_session_notices"))]
#[test]
fn unsupported_notice_is_preserved_as_an_unknown_update() {
use serde_json::json;
let SessionUpdate::Other(notice) = serde_json::from_value(json!({
"sessionUpdate": "notice",
"severity": "warning",
"title": "MCP server unavailable"
}))
.unwrap() else {
panic!("expected unknown session update");
};
assert_eq!(notice.session_update, "notice");
assert_eq!(notice.fields.get("severity"), Some(&json!("warning")));
assert_eq!(
notice.fields.get("title"),
Some(&json!("MCP server unavailable"))
);
}
#[cfg(feature = "unstable_session_compaction")]
#[test]
fn compaction_updates_preserve_patch_and_open_status_semantics() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::CompactionUpdate(
CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
ContentBlock::Text(crate::v2::TextContent::new("retained")),
]),
))
.unwrap(),
json!({
"sessionUpdate": "compaction_update",
"compactionId": "cmp_001",
"status": "completed",
"summary": [{ "type": "text", "text": "retained" }]
})
);
let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
"sessionUpdate": "compaction_update",
"compactionId": "cmp_001",
"status": "paused",
"summary": null
}))
.unwrap() else {
panic!("expected compaction update");
};
assert_eq!(update.status, CompactionStatus::Other("paused".into()));
assert!(update.summary.is_null());
assert!(update.error.is_undefined());
}
#[cfg(feature = "unstable_session_compaction")]
#[test]
fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
use serde_json::json;
assert!(
serde_json::from_value::<SessionUpdate>(json!({
"sessionUpdate": "compaction_update",
"status": "completed"
}))
.is_err()
);
assert!(
serde_json::from_value::<SessionUpdate>(json!({
"sessionUpdate": "compaction_summary_chunk",
"compactionId": "cmp_001"
}))
.is_err()
);
}
#[test]
fn test_elicitation_capability_semantics() {
use serde_json::json;
let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
assert!(unsupported.elicitation.is_none());
let null: ClientCapabilities =
serde_json::from_value(json!({ "elicitation": null })).unwrap();
assert!(null.elicitation.is_none());
let malformed: ClientCapabilities =
serde_json::from_value(json!({ "elicitation": false })).unwrap();
assert!(malformed.elicitation.is_none());
let empty: ClientCapabilities =
serde_json::from_value(json!({ "elicitation": {} })).unwrap();
let empty = empty.elicitation.expect("present capability");
assert!(!empty.supports_form());
assert!(!empty.supports_url());
let form_only: ClientCapabilities = serde_json::from_value(json!({
"elicitation": { "form": {} }
}))
.unwrap();
let form_only = form_only.elicitation.expect("advertised capability");
assert!(form_only.supports_form());
assert!(!form_only.supports_url());
let url_only: ClientCapabilities = serde_json::from_value(json!({
"elicitation": { "url": {} }
}))
.unwrap();
let url_only = url_only.elicitation.expect("advertised capability");
assert!(!url_only.supports_form());
assert!(url_only.supports_url());
let both: ClientCapabilities = serde_json::from_value(json!({
"elicitation": { "form": {}, "url": {} }
}))
.unwrap();
let both = both.elicitation.expect("advertised capability");
assert!(both.supports_form());
assert!(both.supports_url());
}
#[test]
fn test_elicitation_method_routing_and_envelopes() {
use serde_json::json;
assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
assert_eq!(
CLIENT_METHOD_NAMES.elicitation_complete,
"elicitation/complete"
);
let request =
AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
crate::v2::ElicitationFormMode::new(
crate::v2::ElicitationSessionScope::new("sess_1"),
crate::v2::ElicitationSchema::new(),
),
"Choose a value",
)));
assert_eq!(request.method(), "elicitation/create");
let method = Arc::from(request.method());
let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
id: crate::v2::RequestId::Number(7),
method,
params: Some(request),
});
assert_eq!(
serde_json::to_value(request).unwrap(),
json!({
"jsonrpc": "2.0",
"id": 7,
"method": "elicitation/create",
"params": {
"mode": "form",
"sessionId": "sess_1",
"message": "Choose a value",
"requestedSchema": { "type": "object", "properties": {} }
}
})
);
let notification = AgentNotification::CompleteElicitationNotification(Box::new(
CompleteElicitationNotification::new("elic_1"),
));
assert_eq!(notification.method(), "elicitation/complete");
let method = Arc::from(notification.method());
let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
method,
params: Some(notification),
});
assert_eq!(
serde_json::to_value(notification).unwrap(),
json!({
"jsonrpc": "2.0",
"method": "elicitation/complete",
"params": { "elicitationId": "elic_1" }
})
);
}
#[test]
fn test_client_capabilities_auth_defaults_on_malformed_value() {
use serde_json::json;
let capabilities: ClientCapabilities = serde_json::from_value(json!({
"auth": false
}))
.unwrap();
assert_eq!(capabilities.auth, None);
}
#[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: MaybeUndefined::Undefined
}
);
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
.unwrap(),
SessionInfoUpdate {
title: MaybeUndefined::Null,
updated_at: MaybeUndefined::Null,
meta: MaybeUndefined::Undefined
}
);
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: MaybeUndefined::Undefined
}
);
let clear_meta =
serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
assert_eq!(clear_meta.meta, MaybeUndefined::Null);
let mut meta = Meta::new();
meta.insert("source".to_string(), json!("session-info"));
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
"source": "session-info"
}}))
.unwrap()
.meta,
MaybeUndefined::Value(meta.clone())
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
json!({})
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
json!({"_meta": null})
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
json!({"_meta": {
"source": "session-info"
}})
);
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_tool_call_content_chunk_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::ToolCallContentChunk(
ToolCallContentChunk::new(
"call_001",
crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
)
))
.unwrap(),
json!({
"sessionUpdate": "tool_call_content_chunk",
"toolCallId": "call_001",
"content": {
"type": "content",
"content": {
"type": "text",
"text": "partial output"
}
}
})
);
let err = serde_json::from_value::<ToolCallContentChunk>(json!({
"content": {
"type": "content",
"content": {
"type": "text",
"text": "partial output"
}
}
}))
.unwrap_err();
assert!(err.to_string().contains("toolCallId"), "{err}");
}
#[test]
fn test_full_message_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::UserMessage(
UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
crate::v2::TextContent::new("Hello")
)])
))
.unwrap(),
json!({
"sessionUpdate": "user_message",
"messageId": "msg_user_8f7a1",
"content": [
{
"type": "text",
"text": "Hello"
}
]
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::AgentMessage(
AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
crate::v2::TextContent::new("Hello")
)])
))
.unwrap(),
json!({
"sessionUpdate": "agent_message",
"messageId": "msg_agent_c42b9",
"content": [
{
"type": "text",
"text": "Hello"
}
]
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::AgentThought(
AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
crate::v2::TextContent::new("Need to inspect the call sites first.")
)])
))
.unwrap(),
json!({
"sessionUpdate": "agent_thought",
"messageId": "msg_thought_a12",
"content": [
{
"type": "text",
"text": "Need to inspect the call sites first."
}
]
})
);
}
#[test]
fn test_message_upsert_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::UserMessage(
UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
))
.unwrap(),
json!({
"sessionUpdate": "user_message",
"messageId": "msg_empty",
"content": []
})
);
let empty = serde_json::from_value::<UserMessage>(json!({
"messageId": "msg_empty",
"content": []
}))
.unwrap();
assert!(matches!(
empty.content,
MaybeUndefined::Value(ref content) if content.is_empty()
));
let patch = serde_json::from_value::<AgentMessage>(json!({
"messageId": "msg_agent_c42b9"
}))
.unwrap();
assert_eq!(patch.content, MaybeUndefined::Undefined);
assert_eq!(patch.meta, MaybeUndefined::Undefined);
let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
"messageId": "msg_agent_c42b9",
"_meta": false
}))
.unwrap();
assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
let patch = serde_json::from_value::<AgentThought>(json!({
"messageId": "msg_thought_a12"
}))
.unwrap();
assert_eq!(patch.content, MaybeUndefined::Undefined);
let clear = serde_json::from_value::<UserMessage>(json!({
"messageId": "msg_user_8f7a1",
"content": null
}))
.unwrap();
assert_eq!(clear.content, MaybeUndefined::Null);
let clear_meta = serde_json::from_value::<UserMessage>(json!({
"messageId": "msg_user_8f7a1",
"_meta": null
}))
.unwrap();
assert_eq!(clear_meta.meta, MaybeUndefined::Null);
let mut meta = Meta::new();
meta.insert("source".to_string(), json!("replay"));
assert_eq!(
serde_json::to_value(SessionUpdate::UserMessage(
UserMessage::new("msg_user_8f7a1").meta(meta)
))
.unwrap(),
json!({
"sessionUpdate": "user_message",
"messageId": "msg_user_8f7a1",
"_meta": {
"source": "replay"
}
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::UserMessage(
UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
))
.unwrap(),
json!({
"sessionUpdate": "user_message",
"messageId": "msg_user_8f7a1",
"_meta": null
})
);
}
#[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": 200_000
})
);
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": 200_000,
"cost": {
"amount": 0.045,
"currency": "USD"
}
})
);
let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
"sessionUpdate": "usage_update",
"used": 53000,
"size": 200_000,
"cost": null
}))
.unwrap() else {
panic!("expected usage update");
};
assert_eq!(update.cost, None);
}
#[test]
fn test_state_update_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
RunningStateUpdate::new()
)))
.unwrap(),
json!({
"sessionUpdate": "state_update",
"state": "running"
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
)))
.unwrap(),
json!({
"sessionUpdate": "state_update",
"state": "idle",
"stopReason": "end_turn"
})
);
let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
"sessionUpdate": "state_update",
"state": "requires_action"
}))
.unwrap() else {
panic!("expected state update");
};
assert!(matches!(update, StateUpdate::RequiresAction(_)));
let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
"sessionUpdate": "state_update",
"state": "idle",
"stopReason": null
}))
.unwrap() else {
panic!("expected idle state update");
};
assert_eq!(update.stop_reason, None);
let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
serde_json::from_value(json!({
"sessionUpdate": "state_update",
"state": "_paused",
"label": "Paused"
}))
.unwrap()
else {
panic!("expected unknown state update");
};
assert_eq!(update.state, "_paused");
assert_eq!(update.fields["label"], json!("Paused"));
}
#[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 terminal_session_updates_use_known_discriminators() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::TerminalUpdate(
TerminalUpdate::new("term_1").command("cargo test")
))
.unwrap(),
json!({
"sessionUpdate": "terminal_update",
"terminalId": "term_1",
"command": "cargo test"
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::TerminalOutputChunk(
TerminalOutputChunk::new("term_1", "dGVzdAo=")
))
.unwrap(),
json!({
"sessionUpdate": "terminal_output_chunk",
"terminalId": "term_1",
"data": "dGVzdAo="
})
);
}
#[test]
fn session_update_does_not_hide_malformed_known_terminal_variants() {
use serde_json::json;
assert!(
serde_json::from_value::<SessionUpdate>(json!({
"sessionUpdate": "terminal_update"
}))
.is_err()
);
assert!(
serde_json::from_value::<SessionUpdate>(json!({
"sessionUpdate": "terminal_output_chunk",
"terminalId": "term_1"
}))
.is_err()
);
}
#[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",
"planId": "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",
"planId": "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_text_uses_type_discriminator() {
use serde_json::json;
let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
let json = serde_json::to_value(&input).unwrap();
assert_eq!(
json,
json!({
"type": "text",
"hint": "Describe changes"
})
);
let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
}
#[test]
fn request_permission_subject_tool_call_uses_type_discriminator() {
use serde_json::json;
let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
let json = serde_json::to_value(&subject).unwrap();
assert_eq!(
json,
json!({
"type": "tool_call",
"toolCall": {
"toolCallId": "call_001"
}
})
);
let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
assert!(matches!(
roundtripped,
RequestPermissionSubject::ToolCall(_)
));
}
#[test]
fn request_permission_subject_command_uses_type_discriminator() {
use serde_json::json;
let mut meta = Meta::new();
meta.insert("source".to_string(), json!("shell"));
let subject = RequestPermissionSubject::from(
CommandPermissionSubject::new("cargo test", "/workspace/project")
.tool_call_id("call_001")
.terminal_id("term_1")
.meta(meta),
);
let json = serde_json::to_value(&subject).unwrap();
assert_eq!(
json,
json!({
"type": "command",
"command": "cargo test",
"cwd": "/workspace/project",
"toolCallId": "call_001",
"terminalId": "term_1",
"_meta": {
"source": "shell"
}
})
);
let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
}
#[test]
fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
use serde_json::json;
let subject: RequestPermissionSubject = serde_json::from_value(json!({
"type": "command",
"command": "cargo test",
"cwd": "/workspace/project",
"toolCallId": null,
"terminalId": null,
"_meta": null
}))
.unwrap();
let RequestPermissionSubject::Command(subject) = subject else {
panic!("expected command permission subject");
};
assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
assert_eq!(subject.tool_call_id, None);
assert_eq!(subject.terminal_id, None);
assert_eq!(subject.meta, None);
assert_eq!(
serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
json!({
"type": "command",
"command": "cargo test",
"cwd": "/workspace/project"
})
);
}
#[test]
fn request_permission_subject_preserves_unknown_variant() {
use serde_json::json;
let subject: RequestPermissionSubject = serde_json::from_value(json!({
"type": "_review",
"reason": "needs-review",
"retryAfterSeconds": 30
}))
.unwrap();
let RequestPermissionSubject::Other(unknown) = subject else {
panic!("expected unknown permission subject");
};
assert_eq!(unknown.type_, "_review");
assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
assert_eq!(
serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
json!({
"type": "_review",
"reason": "needs-review",
"retryAfterSeconds": 30
})
);
}
#[test]
fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
use serde_json::json;
assert!(
serde_json::from_value::<RequestPermissionSubject>(json!({
"type": "tool_call"
}))
.is_err()
);
assert!(
serde_json::from_value::<RequestPermissionSubject>(json!({
"type": 1
}))
.is_err()
);
assert!(
serde_json::from_value::<RequestPermissionSubject>(json!({
"type": "command",
"cwd": "/workspace/project"
}))
.is_err()
);
assert!(
serde_json::from_value::<RequestPermissionSubject>(json!({
"type": "command",
"command": "cargo test"
}))
.is_err()
);
assert!(
serde_json::from_value::<RequestPermissionSubject>(json!({
"type": "command",
"command": "cargo test",
"cwd": null
}))
.is_err()
);
}
#[test]
fn request_permission_title_and_description_are_separate_from_tool_call_content() {
use serde_json::json;
let request =
RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
.description("Allow this tool to edit src/main.rs?")
.subject(RequestPermissionSubject::from(ToolCallUpdate::new(
"call_001",
)));
assert_eq!(
serde_json::to_value(request).unwrap(),
json!({
"sessionId": "sess_abc123def456",
"title": "Approve file edit?",
"description": "Allow this tool to edit src/main.rs?",
"subject": {
"type": "tool_call",
"toolCall": {
"toolCallId": "call_001"
}
},
"options": []
})
);
}
#[test]
fn request_permission_requires_title_and_allows_missing_subject() {
use serde_json::json;
let request = RequestPermissionRequest::new(
"sess_abc123def456",
"Approve elevated permissions?",
Vec::new(),
);
assert_eq!(
serde_json::to_value(request).unwrap(),
json!({
"sessionId": "sess_abc123def456",
"title": "Approve elevated permissions?",
"options": []
})
);
let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
"sessionId": "sess_abc123def456",
"title": "Approve elevated permissions?",
"options": []
}))
.unwrap();
assert!(missing_subject.subject.is_none());
let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
"sessionId": "sess_abc123def456",
"title": "Approve elevated permissions?",
"subject": null,
"options": []
}))
.unwrap();
assert!(null_subject.subject.is_none());
assert!(
serde_json::from_value::<RequestPermissionRequest>(json!({
"sessionId": "sess_abc123def456",
"options": []
}))
.is_err()
);
}
#[test]
fn request_permission_outcome_preserves_unknown_variant() {
use serde_json::json;
let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
"outcome": "_defer",
"reason": "needs-review",
"retryAfterSeconds": 30
}))
.unwrap();
let RequestPermissionOutcome::Other(unknown) = outcome else {
panic!("expected unknown permission outcome");
};
assert_eq!(unknown.outcome, "_defer");
assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
assert_eq!(
serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
json!({
"outcome": "_defer",
"reason": "needs-review",
"retryAfterSeconds": 30
})
);
}
#[test]
fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
use serde_json::json;
assert!(
serde_json::from_value::<RequestPermissionOutcome>(json!({
"outcome": "selected"
}))
.is_err()
);
assert!(
serde_json::from_value::<RequestPermissionOutcome>(json!({
"outcome": 1
}))
.is_err()
);
}
#[test]
fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
use serde_json::json;
assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
assert!(
serde_json::from_value::<AvailableCommandInput>(json!({
"hint": "Pick one"
}))
.is_err()
);
assert!(
serde_json::from_value::<AvailableCommandInput>(json!({
"type": 1,
"hint": "Pick one"
}))
.is_err()
);
assert!(
serde_json::from_value::<OtherAvailableCommandInput>(json!({
"type": "text",
"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_message, "mcp/message");
assert_eq!(
AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
"server-1",
"req-1",
"tools/list"
)))
.method(),
"mcp/message"
);
assert_eq!(
serde_json::to_value(
MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
)
.unwrap(),
json!({
"serverId": "server-1",
"requestId": "req-1",
"method": "tools/list",
"params": { "cursor": "abc" }
})
);
let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
"serverId": "server-1",
"requestId": "req-1",
"method": "tools/list",
"params": null,
"_meta": null
}))
.unwrap();
assert_eq!(request_with_null_params.params, None);
assert_eq!(request_with_null_params.meta, None);
for key in ["serverId", "requestId", "method"] {
let mut value =
json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
value.as_object_mut().unwrap().remove(key);
assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
}
for key in ["serverId", "requestId", "method"] {
let mut value =
json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
value[key] = serde_json::Value::Null;
assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
}
}
#[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());
}
#[test]
fn request_permission_request_rejects_malformed_options() {
use serde_json::json;
assert!(
serde_json::from_value::<RequestPermissionRequest>(json!({
"sessionId": "sess-1",
"title": "Run tool?",
"options": "not-an-array"
}))
.is_err()
);
assert!(
serde_json::from_value::<RequestPermissionRequest>(json!({
"sessionId": "sess-1",
"title": "Run tool?",
"options": [{"optionId": "allow"}]
}))
.is_err()
);
}
#[cfg(feature = "unstable_plan_operations")]
#[test]
fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
use serde_json::json;
assert!(
serde_json::from_value::<SessionUpdate>(json!({
"sessionUpdate": "plan_removed"
}))
.is_err()
);
}
}