use std::{path::PathBuf, sync::Arc};
use derive_more::{Display, From};
#[cfg(all(feature = "schemars", feature = "unstable_subagents"))]
use schemars::Schema;
use serde::{Deserialize, Serialize};
use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
#[cfg(feature = "unstable_subagents")]
use std::collections::BTreeMap;
#[cfg(feature = "unstable_subagents")]
use super::StopReason;
#[cfg(all(
feature = "unstable_subagents",
feature = "unstable_end_turn_token_usage"
))]
use super::Usage;
use super::{
CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse,
ElicitationCapabilities,
};
use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined};
use super::{
ContentBlock, EnvVariable, ExtNotification, ExtRequest, ExtResponse, Meta, Plan,
SessionConfigOption, SessionId, SessionModeId, ToolCall, ToolCallUpdate,
};
#[cfg(feature = "unstable_plan_operations")]
use super::{PlanCapabilities, PlanRemoved, PlanUpdate};
#[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 SessionNotification {
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 SessionNotification {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
Self {
session_id: session_id.into(),
update,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", schemars(extend("discriminator" = {"propertyName": "sessionUpdate"})))]
#[non_exhaustive]
pub enum SessionUpdate {
UserMessageChunk(ContentChunk),
AgentMessageChunk(ContentChunk),
AgentThoughtChunk(ContentChunk),
ToolCall(ToolCall),
ToolCallUpdate(ToolCallUpdate),
Plan(Plan),
#[cfg(feature = "unstable_plan_operations")]
PlanUpdate(PlanUpdate),
#[cfg(feature = "unstable_plan_operations")]
PlanRemoved(PlanRemoved),
AvailableCommandsUpdate(AvailableCommandsUpdate),
CurrentModeUpdate(CurrentModeUpdate),
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),
}
#[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<_>>>")]
#[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 envelopes_preserve_participants_and_multimodal_content() {
let content = json!([
{"type": "text", "text": "Please inspect this"},
{"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"}
]);
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": content
}});
let decoded: SessionNotification = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(&decoded).unwrap(), wire);
assert!(matches!(decoded.update, SessionUpdate::SessionMessage(_)));
let message = SessionMessage::new(id)
.sender_session_id(SessionId::new(sender))
.recipient_session_id(SessionId::new(recipient))
.content(vec![]);
assert_eq!(serde_json::to_value(message).unwrap()["content"], json!([]));
}
}
#[test]
fn message_upserts_validate_ids_and_patch_fields() {
let base = json!({"sessionUpdate": "session_message", "messageId": "m1",
"senderSessionId": "parent", "recipientSessionId": "child", "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(),
"{key}"
);
let mut null = base.clone();
null[key] = Value::Null;
assert!(
serde_json::from_value::<SessionUpdate>(null).is_err(),
"{key}"
);
let mut non_string = base.clone();
non_string[key] = json!(42);
assert!(
serde_json::from_value::<SessionUpdate>(non_string).is_err(),
"{key}"
);
}
for content in [None, 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).unwrap();
let encoded = serde_json::to_value(decoded).unwrap();
let mut unchanged = base.clone();
unchanged.as_object_mut().unwrap().remove("content");
assert_eq!(encoded, unchanged);
}
let clear = SessionMessage::new("m1")
.sender_session_id(SessionId::new("parent"))
.recipient_session_id(SessionId::new("child"))
.content(None)
.meta(None);
let mut clear_wire = base.clone();
clear_wire["content"] = Value::Null;
clear_wire["_meta"] = Value::Null;
assert_eq!(
serde_json::to_value(&clear).unwrap()["content"],
Value::Null
);
let clear_update: SessionUpdate = serde_json::from_value(clear_wire.clone()).unwrap();
assert_eq!(serde_json::to_value(clear_update).unwrap(), clear_wire);
assert!(clear.content.is_null());
assert!(clear.meta.is_null());
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();
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 unset = SessionMessage::new("m1");
assert_eq!(
serde_json::to_value(&unset).unwrap(),
json!({"messageId": "m1"})
);
assert!(unset.content.is_undefined());
assert!(unset.meta.is_undefined());
let reset = SessionMessage::new("m1").content(vec![]);
assert_eq!(serde_json::to_value(reset).unwrap()["content"], json!([]));
let malformed: SessionMessage = serde_json::from_value(json!({
"messageId": "m1", "senderSessionId": "parent", "recipientSessionId": "child",
"content": false, "_meta": false
}))
.unwrap();
assert!(malformed.content.is_undefined());
assert!(malformed.meta.is_undefined());
}
#[test]
fn first_chunk_and_upsert_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": "first"}
}});
let decoded: SessionNotification = 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"));
let chunk = SessionMessageChunk::new(
id,
ContentBlock::Text(crate::v1::TextContent::new("first")),
)
.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"]
);
}
let wire = json!({"sessionId": "child", "update": {
"sessionUpdate": "session_message_chunk", "messageId": "received-9",
"senderSessionId": "parent", "recipientSessionId": "child",
"content": {"type": "text", "text": "first"}
}});
let base = wire["update"].clone();
for key in ["messageId", "content"] {
let mut missing = base.clone();
missing.as_object_mut().unwrap().remove(key);
assert!(
serde_json::from_value::<SessionUpdate>(missing).is_err(),
"{key}"
);
let mut null = base.clone();
null[key] = Value::Null;
assert!(
serde_json::from_value::<SessionUpdate>(null).is_err(),
"{key}"
);
let mut non_string = base.clone();
non_string[key] = json!(42);
assert!(
serde_json::from_value::<SessionUpdate>(non_string).is_err(),
"{key}"
);
}
for meta in [None, Some(Value::Null), Some(json!({"chunk": true}))] {
let mut value = base.clone();
if let Some(meta) = meta {
value["_meta"] = meta;
}
let decoded: SessionUpdate = serde_json::from_value(value.clone()).unwrap();
let encoded = serde_json::to_value(decoded).unwrap();
if value["_meta"].is_null() {
assert_eq!(encoded, base);
} else {
assert_eq!(encoded, value);
}
}
}
#[cfg(feature = "schemars")]
#[test]
fn schema_requires_ids_and_chunk_content() {
let schema = serde_json::to_value(schemars::schema_for!(SessionMessage)).unwrap();
let required = schema["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::v1::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 message_updates_require_subagents_gate() {
for (kind, content) in [
("session_message", json!([])),
(
"session_message_chunk",
json!({"type": "text", "text": "hello"}),
),
] {
let wire = json!({"sessionUpdate": kind, "messageId": "m1", "content": content});
assert!(serde_json::from_value::<SessionUpdate>(wire).is_err());
}
}
}
#[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(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct CompactionId(pub Arc<str>);
#[cfg(feature = "unstable_session_compaction")]
impl CompactionId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> 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<_>>>")]
#[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<MaybeUndefined<_>>")]
#[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<MaybeUndefined<_>>")]
#[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<MaybeUndefined<_>>")]
#[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(
default,
rename = "_meta",
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(feature = "unstable_subagents")]
#[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),
Unknown(UnknownStateUpdate),
#[serde(untagged)]
Other(OtherStateUpdate),
}
#[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 RunningStateUpdate {
#[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 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
}
}
#[cfg(feature = "unstable_subagents")]
#[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, rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_subagents")]
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
}
}
#[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 RequiresActionStateUpdate {
#[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 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(feature = "unstable_subagents")]
#[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 {
pub state: String,
#[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>,
}
#[cfg(feature = "unstable_subagents")]
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,
}
}
}
#[cfg(feature = "unstable_subagents")]
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 })
}
}
#[cfg(feature = "unstable_subagents")]
const KNOWN_STATE_UPDATE_STATES: &[&str] = &["running", "idle", "requires_action", "unknown"];
#[cfg(feature = "unstable_subagents")]
fn is_known_state_update(state: &str) -> bool {
KNOWN_STATE_UPDATE_STATES.contains(&state)
}
#[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
fn other_state_update_schema(schema: &mut Schema) {
let known = KNOWN_STATE_UPDATE_STATES
.iter()
.map(|state| {
serde_json::json!({
"properties": { "state": { "const": state, "type": "string" } },
"required": ["state"],
"type": "object"
})
})
.collect::<Vec<_>>();
schema.insert("not".into(), serde_json::json!({ "anyOf": known }));
}
#[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 CurrentModeUpdate {
pub current_mode_id: SessionModeId,
#[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 CurrentModeUpdate {
#[must_use]
pub fn new(current_mode_id: impl Into<SessionModeId>) -> Self {
Self {
current_mode_id: current_mode_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, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ConfigOptionUpdate {
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_>>")]
#[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)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub updated_at: MaybeUndefined<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 SessionInfoUpdate {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
self.title = title.into_maybe_undefined();
self
}
#[must_use]
pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
self.updated_at = updated_at.into_maybe_undefined();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[serde_as]
#[skip_serializing_none]
#[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
}
}
#[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,
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 content: ContentBlock,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub message_id: Option<MessageId>,
#[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) -> Self {
Self {
content,
message_id: None,
meta: None,
}
}
#[must_use]
pub fn message_id(mut self, message_id: impl IntoOption<MessageId>) -> Self {
self.message_id = message_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, Deserialize, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct MessageId(pub Arc<str>);
impl MessageId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
impl IntoOption<MessageId> for &str {
fn into_option(self) -> Option<MessageId> {
Some(MessageId::new(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 AvailableCommandsUpdate {
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_>>")]
#[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(untagged, rename_all = "camelCase")]
#[non_exhaustive]
pub enum AvailableCommandInput {
Unstructured(UnstructuredCommandInput),
}
#[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 UnstructuredCommandInput {
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 UnstructuredCommandInput {
#[must_use]
pub fn new(hint: impl Into<String>) -> Self {
Self {
hint: hint.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[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 tool_call: ToolCallUpdate,
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>,
tool_call: ToolCallUpdate,
options: Vec<PermissionOption>,
) -> Self {
Self {
session_id: session_id.into(),
tool_call,
options,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[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(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct PermissionOptionId(pub Arc<str>);
impl PermissionOptionId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PermissionOptionKind {
AllowOnce,
AllowAlways,
RejectOnce,
RejectAlways,
}
#[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")]
#[cfg_attr(feature = "schemars", schemars(extend("discriminator" = {"propertyName": "outcome"})))]
#[non_exhaustive]
pub enum RequestPermissionOutcome {
Cancelled,
#[serde(rename_all = "camelCase")]
Selected(SelectedPermissionOutcome),
}
#[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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = FS_WRITE_TEXT_FILE_METHOD_NAME)))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct WriteTextFileRequest {
pub session_id: SessionId,
pub path: PathBuf,
pub content: 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 WriteTextFileRequest {
#[must_use]
pub fn new(
session_id: impl Into<SessionId>,
path: impl Into<PathBuf>,
content: impl Into<String>,
) -> Self {
Self {
session_id: session_id.into(),
path: path.into(),
content: content.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
crate::serde_util::default_on_null! {
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = FS_WRITE_TEXT_FILE_METHOD_NAME)))]
#[non_exhaustive]
pub struct WriteTextFileResponse {
#[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 WriteTextFileResponse {
#[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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = FS_READ_TEXT_FILE_METHOD_NAME)))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ReadTextFileRequest {
pub session_id: SessionId,
pub path: PathBuf,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub line: Option<u32>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub limit: Option<u32>,
#[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 ReadTextFileRequest {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, path: impl Into<PathBuf>) -> Self {
Self {
session_id: session_id.into(),
path: path.into(),
line: None,
limit: None,
meta: None,
}
}
#[must_use]
pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
self.line = line.into_option();
self
}
#[must_use]
pub fn limit(mut self, limit: impl IntoOption<u32>) -> Self {
self.limit = limit.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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = FS_READ_TEXT_FILE_METHOD_NAME)))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ReadTextFileResponse {
pub content: 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 ReadTextFileResponse {
#[must_use]
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
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(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct TerminalId(pub Arc<str>);
impl TerminalId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> 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")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_CREATE_METHOD_NAME)))]
#[non_exhaustive]
pub struct CreateTerminalRequest {
pub session_id: SessionId,
pub command: String,
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_>>")]
#[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 args: Vec<String>,
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_>>")]
#[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 env: Vec<EnvVariable>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub cwd: Option<PathBuf>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub output_byte_limit: Option<u64>,
#[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 CreateTerminalRequest {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, command: impl Into<String>) -> Self {
Self {
session_id: session_id.into(),
command: command.into(),
args: Vec::new(),
env: Vec::new(),
cwd: None,
output_byte_limit: None,
meta: None,
}
}
#[must_use]
pub fn args(mut self, args: Vec<String>) -> Self {
self.args = args;
self
}
#[must_use]
pub fn env(mut self, env: Vec<EnvVariable>) -> Self {
self.env = env;
self
}
#[must_use]
pub fn cwd(mut self, cwd: impl IntoOption<PathBuf>) -> Self {
self.cwd = cwd.into_option();
self
}
#[must_use]
pub fn output_byte_limit(mut self, output_byte_limit: impl IntoOption<u64>) -> Self {
self.output_byte_limit = output_byte_limit.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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_CREATE_METHOD_NAME)))]
#[non_exhaustive]
pub struct CreateTerminalResponse {
pub terminal_id: 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 CreateTerminalResponse {
#[must_use]
pub fn new(terminal_id: impl Into<TerminalId>) -> Self {
Self {
terminal_id: terminal_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, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_OUTPUT_METHOD_NAME)))]
#[non_exhaustive]
pub struct TerminalOutputRequest {
pub session_id: SessionId,
pub terminal_id: 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 TerminalOutputRequest {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, terminal_id: impl Into<TerminalId>) -> Self {
Self {
session_id: session_id.into(),
terminal_id: terminal_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, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_OUTPUT_METHOD_NAME)))]
#[non_exhaustive]
pub struct TerminalOutputResponse {
pub output: String,
pub truncated: bool,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub exit_status: Option<TerminalExitStatus>,
#[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 TerminalOutputResponse {
#[must_use]
pub fn new(output: impl Into<String>, truncated: bool) -> Self {
Self {
output: output.into(),
truncated,
exit_status: None,
meta: None,
}
}
#[must_use]
pub fn exit_status(mut self, exit_status: impl IntoOption<TerminalExitStatus>) -> Self {
self.exit_status = exit_status.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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_RELEASE_METHOD_NAME)))]
#[non_exhaustive]
pub struct ReleaseTerminalRequest {
pub session_id: SessionId,
pub terminal_id: 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 ReleaseTerminalRequest {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, terminal_id: impl Into<TerminalId>) -> Self {
Self {
session_id: session_id.into(),
terminal_id: terminal_id.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
crate::serde_util::default_on_null! {
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_RELEASE_METHOD_NAME)))]
#[non_exhaustive]
pub struct ReleaseTerminalResponse {
#[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 ReleaseTerminalResponse {
#[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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_KILL_METHOD_NAME)))]
#[non_exhaustive]
pub struct KillTerminalRequest {
pub session_id: SessionId,
pub terminal_id: 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 KillTerminalRequest {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, terminal_id: impl Into<TerminalId>) -> Self {
Self {
session_id: session_id.into(),
terminal_id: terminal_id.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
crate::serde_util::default_on_null! {
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_KILL_METHOD_NAME)))]
#[non_exhaustive]
pub struct KillTerminalResponse {
#[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 KillTerminalResponse {
#[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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_WAIT_FOR_EXIT_METHOD_NAME)))]
#[non_exhaustive]
pub struct WaitForTerminalExitRequest {
pub session_id: SessionId,
pub terminal_id: 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 WaitForTerminalExitRequest {
#[must_use]
pub fn new(session_id: impl Into<SessionId>, terminal_id: impl Into<TerminalId>) -> Self {
Self {
session_id: session_id.into(),
terminal_id: terminal_id.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
crate::serde_util::default_on_null! {
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = TERMINAL_WAIT_FOR_EXIT_METHOD_NAME)))]
#[non_exhaustive]
pub struct WaitForTerminalExitResponse {
#[serde(flatten)]
pub exit_status: TerminalExitStatus,
#[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 WaitForTerminalExitResponse {
#[must_use]
pub fn new(exit_status: TerminalExitStatus) -> Self {
Self {
exit_status,
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 TerminalExitStatus {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub exit_code: Option<u32>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub signal: Option<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 TerminalExitStatus {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn exit_code(mut self, exit_code: impl IntoOption<u32>) -> Self {
self.exit_code = exit_code.into_option();
self
}
#[must_use]
pub fn signal(mut self, signal: impl IntoOption<String>) -> Self {
self.signal = signal.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 ClientCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub fs: FileSystemCapabilities,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub terminal: bool,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub session: Option<ClientSessionCapabilities>,
#[cfg(feature = "unstable_subagents")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub subagents: Option<SubagentCapabilities>,
#[cfg(feature = "unstable_plan_operations")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub plan: Option<PlanCapabilities>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub auth: 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<_>>")]
#[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 fs(mut self, fs: FileSystemCapabilities) -> Self {
self.fs = fs;
self
}
#[must_use]
pub fn terminal(mut self, terminal: bool) -> Self {
self.terminal = terminal;
self
}
#[must_use]
pub fn session(mut self, session: impl IntoOption<ClientSessionCapabilities>) -> Self {
self.session = session.into_option();
self
}
#[cfg(feature = "unstable_subagents")]
#[must_use]
pub fn subagents(mut self, subagents: impl IntoOption<SubagentCapabilities>) -> Self {
self.subagents = subagents.into_option();
self
}
#[cfg(feature = "unstable_plan_operations")]
#[must_use]
pub fn plan(mut self, plan: impl IntoOption<PlanCapabilities>) -> Self {
self.plan = plan.into_option();
self
}
#[must_use]
pub fn auth(mut self, auth: AuthCapabilities) -> Self {
self.auth = auth;
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
}
}
#[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 SubagentCapabilities {
#[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 SubagentCapabilities {
#[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, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ClientSessionCapabilities {
#[cfg(feature = "unstable_session_compaction")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub compaction: Option<CompactionCapabilities>,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub config_options: Option<SessionConfigOptionsCapabilities>,
#[cfg(feature = "unstable_session_notices")]
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub notices: Option<NoticeCapabilities>,
#[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 ClientSessionCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "unstable_session_compaction")]
#[must_use]
pub fn compaction(mut self, compaction: impl IntoOption<CompactionCapabilities>) -> Self {
self.compaction = compaction.into_option();
self
}
#[must_use]
pub fn config_options(
mut self,
config_options: impl IntoOption<SessionConfigOptionsCapabilities>,
) -> Self {
self.config_options = config_options.into_option();
self
}
#[cfg(feature = "unstable_session_notices")]
#[must_use]
pub fn notices(mut self, notices: impl IntoOption<NoticeCapabilities>) -> Self {
self.notices = notices.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(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CompactionCapabilities {}
#[cfg(feature = "unstable_session_compaction")]
impl CompactionCapabilities {
#[must_use]
pub fn new() -> Self {
Self {}
}
}
#[cfg(feature = "unstable_session_notices")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct NoticeCapabilities {}
#[cfg(feature = "unstable_session_notices")]
impl NoticeCapabilities {
#[must_use]
pub fn new() -> Self {
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 SessionConfigOptionsCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub boolean: Option<BooleanConfigOptionCapabilities>,
#[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 SessionConfigOptionsCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn boolean(mut self, boolean: impl IntoOption<BooleanConfigOptionCapabilities>) -> Self {
self.boolean = boolean.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 BooleanConfigOptionCapabilities {
#[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 BooleanConfigOptionCapabilities {
#[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, 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: bool,
#[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: bool) -> Self {
self.terminal = terminal;
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 FileSystemCapabilities {
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub read_text_file: bool,
#[serde_as(deserialize_as = "DefaultOnError")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default)]
pub write_text_file: bool,
#[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 FileSystemCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn read_text_file(mut self, read_text_file: bool) -> Self {
self.read_text_file = read_text_file;
self
}
#[must_use]
pub fn write_text_file(mut self, write_text_file: bool) -> Self {
self.write_text_file = write_text_file;
self
}
#[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,
pub fs_write_text_file: &'static str,
pub fs_read_text_file: &'static str,
pub terminal_create: &'static str,
pub terminal_output: &'static str,
pub terminal_release: &'static str,
pub terminal_wait_for_exit: &'static str,
pub terminal_kill: &'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,
fs_write_text_file: FS_WRITE_TEXT_FILE_METHOD_NAME,
fs_read_text_file: FS_READ_TEXT_FILE_METHOD_NAME,
terminal_create: TERMINAL_CREATE_METHOD_NAME,
terminal_output: TERMINAL_OUTPUT_METHOD_NAME,
terminal_release: TERMINAL_RELEASE_METHOD_NAME,
terminal_wait_for_exit: TERMINAL_WAIT_FOR_EXIT_METHOD_NAME,
terminal_kill: TERMINAL_KILL_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 FS_WRITE_TEXT_FILE_METHOD_NAME: &str = "fs/write_text_file";
pub(crate) const FS_READ_TEXT_FILE_METHOD_NAME: &str = "fs/read_text_file";
pub(crate) const TERMINAL_CREATE_METHOD_NAME: &str = "terminal/create";
pub(crate) const TERMINAL_OUTPUT_METHOD_NAME: &str = "terminal/output";
pub(crate) const TERMINAL_RELEASE_METHOD_NAME: &str = "terminal/release";
pub(crate) const TERMINAL_WAIT_FOR_EXIT_METHOD_NAME: &str = "terminal/wait_for_exit";
pub(crate) const TERMINAL_KILL_METHOD_NAME: &str = "terminal/kill";
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]
#[allow(clippy::large_enum_variant)]
pub enum AgentRequest {
WriteTextFileRequest(WriteTextFileRequest),
ReadTextFileRequest(ReadTextFileRequest),
RequestPermissionRequest(RequestPermissionRequest),
CreateTerminalRequest(CreateTerminalRequest),
TerminalOutputRequest(TerminalOutputRequest),
ReleaseTerminalRequest(ReleaseTerminalRequest),
WaitForTerminalExitRequest(WaitForTerminalExitRequest),
KillTerminalRequest(KillTerminalRequest),
CreateElicitationRequest(CreateElicitationRequest),
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpRequest(MessageMcpRequest),
ExtMethodRequest(ExtRequest),
}
impl AgentRequest {
#[must_use]
pub fn method(&self) -> &str {
match self {
Self::WriteTextFileRequest(_) => CLIENT_METHOD_NAMES.fs_write_text_file,
Self::ReadTextFileRequest(_) => CLIENT_METHOD_NAMES.fs_read_text_file,
Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
Self::CreateTerminalRequest(_) => CLIENT_METHOD_NAMES.terminal_create,
Self::TerminalOutputRequest(_) => CLIENT_METHOD_NAMES.terminal_output,
Self::ReleaseTerminalRequest(_) => CLIENT_METHOD_NAMES.terminal_release,
Self::WaitForTerminalExitRequest(_) => CLIENT_METHOD_NAMES.terminal_wait_for_exit,
Self::KillTerminalRequest(_) => CLIENT_METHOD_NAMES.terminal_kill,
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 {
WriteTextFileResponse(#[serde(default)] WriteTextFileResponse),
ReadTextFileResponse(ReadTextFileResponse),
RequestPermissionResponse(RequestPermissionResponse),
CreateTerminalResponse(CreateTerminalResponse),
TerminalOutputResponse(TerminalOutputResponse),
ReleaseTerminalResponse(#[serde(default)] ReleaseTerminalResponse),
WaitForTerminalExitResponse(WaitForTerminalExitResponse),
KillTerminalResponse(#[serde(default)] KillTerminalResponse),
CreateElicitationResponse(CreateElicitationResponse),
#[cfg(feature = "unstable_mcp_over_acp")]
MessageMcpResponse(MessageMcpResponse),
ExtMethodResponse(ExtResponse),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
#[expect(clippy::large_enum_variant)]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[non_exhaustive]
pub enum AgentNotification {
SessionNotification(SessionNotification),
CompleteElicitationNotification(CompleteElicitationNotification),
ExtNotification(ExtNotification),
}
impl AgentNotification {
#[must_use]
pub fn method(&self) -> &str {
match self {
Self::SessionNotification(_) => 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_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"));
assert_eq!(
serde_json::to_value(SessionUpdate::Notice(
Notice::new(NoticeSeverity::Warning, "MCP server unavailable")
.description("Continuing without it.")
.meta(meta),
))
.unwrap(),
json!({
"sessionUpdate": "notice",
"severity": "warning",
"title": "MCP server unavailable",
"description": "Continuing without it.",
"_meta": { "source": "fallback" }
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::Notice(Notice::new(
NoticeSeverity::Info,
"Indexing workspace",
)))
.unwrap(),
json!({
"sessionUpdate": "notice",
"severity": "info",
"title": "Indexing workspace"
})
);
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 notice_requires_non_null_severity_and_title() {
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(feature = "unstable_session_notices")]
#[test]
fn notice_capability_advertises_support_only_when_present() {
use serde_json::json;
let capabilities = ClientCapabilities::new()
.session(ClientSessionCapabilities::new().notices(NoticeCapabilities::new()));
let value = serde_json::to_value(&capabilities).unwrap();
assert_eq!(value["session"], json!({ "notices": {} }));
assert_eq!(
serde_json::from_value::<ClientCapabilities>(value).unwrap(),
capabilities
);
for unsupported in [
json!({}),
json!({ "session": null }),
json!({ "session": {} }),
json!({ "session": { "notices": null } }),
json!({ "session": { "notices": false } }),
json!({ "session": { "notices": true } }),
json!({ "session": { "notices": "supported" } }),
] {
let capabilities: ClientCapabilities = serde_json::from_value(unsupported).unwrap();
assert!(
capabilities
.session
.and_then(|session| session.notices)
.is_none()
);
}
assert_eq!(
serde_json::to_value(
ClientSessionCapabilities::new()
.notices(NoticeCapabilities::new())
.notices(None)
)
.unwrap(),
json!({})
);
}
#[cfg(not(feature = "unstable_session_notices"))]
#[test]
fn unsupported_notice_capability_is_ignored() {
use serde_json::json;
let capabilities: ClientSessionCapabilities =
serde_json::from_value(json!({ "notices": {} })).unwrap();
assert_eq!(serde_json::to_value(capabilities).unwrap(), json!({}));
}
#[cfg(not(feature = "unstable_session_notices"))]
#[test]
fn unsupported_notice_is_rejected_by_closed_update_union() {
use serde_json::json;
assert!(
serde_json::from_value::<SessionUpdate>(json!({
"sessionUpdate": "notice",
"severity": "warning",
"title": "MCP server unavailable"
}))
.is_err()
);
}
#[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::InProgress,
)))
.unwrap(),
json!({
"sessionUpdate": "compaction_update",
"compactionId": "cmp_001",
"status": "in_progress"
})
);
let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
"sessionUpdate": "compaction_update",
"compactionId": "cmp_001",
"status": "paused",
"summary": null,
"error": "waiting"
}))
.unwrap() else {
panic!("expected compaction update");
};
assert_eq!(update.status, CompactionStatus::Other("paused".into()));
assert!(update.summary.is_null());
assert_eq!(update.error.value().map(String::as_str), Some("waiting"));
assert!(update.meta.is_undefined());
}
#[cfg(feature = "unstable_session_compaction")]
#[test]
fn compaction_chunk_and_v1_capability_serialize() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::CompactionSummaryChunk(
CompactionSummaryChunk::new(
"cmp_001",
ContentBlock::Text(crate::v1::TextContent::new("retained")),
),
))
.unwrap(),
json!({
"sessionUpdate": "compaction_summary_chunk",
"compactionId": "cmp_001",
"content": { "type": "text", "text": "retained" }
})
);
assert_eq!(
serde_json::to_value(
ClientSessionCapabilities::new().compaction(CompactionCapabilities::new())
)
.unwrap(),
json!({ "compaction": {} })
);
let absent: ClientSessionCapabilities = serde_json::from_value(json!({})).unwrap();
let null: ClientSessionCapabilities =
serde_json::from_value(json!({ "compaction": null })).unwrap();
assert!(absent.compaction.is_none());
assert!(null.compaction.is_none());
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn test_subagent_updates_serialization() {
use serde_json::json;
let announced = SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1"));
assert_eq!(
serde_json::to_value(&announced).unwrap(),
json!({
"sessionUpdate": "subagent_update",
"sessionId": "sess_child_1"
})
);
assert_eq!(
serde_json::from_value::<SessionUpdate>(serde_json::to_value(&announced).unwrap())
.unwrap(),
announced
);
let capable = SubagentUpdate::new("sess_child_1").capabilities(
SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
);
assert_eq!(
serde_json::to_value(capable).unwrap(),
json!({
"sessionId": "sess_child_1",
"capabilities": {"cancel": {}}
})
);
let minimal: SubagentUpdate =
serde_json::from_value(json!({ "sessionId": "sess_child_3" })).unwrap();
assert!(minimal.capabilities.is_undefined());
assert!(minimal.state.is_undefined());
let nulls: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "sess_child_3",
"capabilities": null,
"state": null
}))
.unwrap();
assert!(nulls.capabilities.is_null());
assert!(nulls.state.is_null());
assert_eq!(
serde_json::to_value(nulls).unwrap(),
json!({"sessionId": "sess_child_3", "capabilities": null, "state": null})
);
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn test_subagent_display_metadata() {
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_eq!(
serde_json::to_value(&minimal).unwrap(),
json!({"sessionId": "child"})
);
let cleared: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "child", "title": null, "description": null
}))
.unwrap();
assert!(cleared.title.is_null());
assert!(cleared.description.is_null());
assert_eq!(
serde_json::to_value(cleared).unwrap(),
json!({"sessionId": "child", "title": null, "description": null})
);
let invalid: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "child", "title": false, "description": false
}))
.unwrap();
assert_eq!(invalid, minimal);
let title_only: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "child", "title": "Updated title"
}))
.unwrap();
assert_eq!(
title_only.title.value().map(String::as_str),
Some("Updated title")
);
assert!(title_only.description.is_undefined());
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn subagent_patch_fields_preserve_omitted_null_and_concrete() {
use serde_json::json;
let omitted = SubagentUpdate::new("child");
for field in ["title", "description", "capabilities", "state", "_meta"] {
let wire = json!({"sessionId": "child", field: null});
let decoded: SubagentUpdate = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), wire, "{field}");
let malformed = json!({"sessionId": "child", field: false});
let decoded: SubagentUpdate = serde_json::from_value(malformed).unwrap();
assert_eq!(decoded, omitted, "{field}");
}
let concrete = json!({
"sessionId": "child", "title": "Investigator",
"description": "Tests", "capabilities": {"cancel": {}},
"state": {"state": "running"}, "_meta": {"source": "parent"}
});
let decoded: SubagentUpdate = serde_json::from_value(concrete.clone()).unwrap();
assert!(decoded.title.value().is_some());
assert!(decoded.description.value().is_some());
assert!(decoded.capabilities.value().is_some());
assert!(decoded.state.value().is_some());
assert!(decoded.meta.value().is_some());
assert_eq!(serde_json::to_value(decoded).unwrap(), concrete);
let cleared = SubagentUpdate::new("child")
.title(None)
.description(None)
.capabilities(None)
.state(None)
.meta(None);
assert_eq!(
serde_json::to_value(cleared).unwrap(),
json!({
"sessionId": "child", "title": null, "description": null,
"capabilities": null, "state": null, "_meta": null
})
);
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn test_subagent_notification_keeps_parent_and_child_ids_nested() {
use serde_json::json;
let notification = SessionNotification::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::<SessionNotification>(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::<SessionNotification>(malformed).is_err());
}
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn test_subagent_states_round_trip() {
use serde_json::json;
for (wire_state, expected) in [
(
json!({"state": "running"}),
StateUpdate::Running(RunningStateUpdate::new()),
),
(
json!({"state": "idle", "stopReason": "end_turn"}),
StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
),
(
json!({"state": "requires_action"}),
StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
),
(
json!({"state": "unknown"}),
StateUpdate::Unknown(UnknownStateUpdate::new()),
),
(
json!({"state": "_waiting", "detail": {"ticket": 7}}),
StateUpdate::Other(OtherStateUpdate::new(
"_waiting",
[("detail".into(), json!({"ticket": 7}))].into(),
)),
),
(
json!({"state": "paused", "retryAt": 42}),
StateUpdate::Other(OtherStateUpdate::new(
"paused",
[("retryAt".into(), json!(42))].into(),
)),
),
] {
let wire = json!({
"sessionUpdate": "subagent_update",
"sessionId": "sess_child",
"state": wire_state
});
let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
let SessionUpdate::SubagentUpdate(update) = &parsed else {
panic!("expected subagent update");
};
assert_eq!(update.state.value(), Some(&expected));
assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
}
for reason in [
"end_turn",
"max_tokens",
"max_turn_requests",
"refusal",
"cancelled",
] {
let state: StateUpdate = serde_json::from_value(json!({
"state": "idle", "stopReason": reason
}))
.unwrap();
assert_eq!(serde_json::to_value(state).unwrap()["stopReason"], reason);
}
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::Unknown(UnknownStateUpdate::new()),
StateUpdate::Running(RunningStateUpdate::new()),
] {
let update = SubagentUpdate::new("sess_child").state(state.clone());
let wire = serde_json::to_value(&update).unwrap();
assert_eq!(wire["sessionId"], "sess_child");
assert_eq!(
serde_json::from_value::<SubagentUpdate>(wire)
.unwrap()
.state,
MaybeUndefined::Value(state)
);
}
assert_eq!(
serde_json::to_value(
SubagentUpdate::new("sess_child").state(StateUpdate::Idle(IdleStateUpdate::new()))
)
.unwrap()["state"],
json!({"state": "idle"})
);
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn test_subagent_unknown_activity_metadata_and_capabilities() {
use serde_json::json;
let update = SubagentUpdate::new("child").state(StateUpdate::Unknown(
UnknownStateUpdate::new().meta(
[("source".into(), json!("worker"))]
.into_iter()
.collect::<Meta>(),
),
));
let wire = json!({
"sessionId": "child",
"state": {
"state": "unknown",
"_meta": { "source": "worker" }
}
});
assert_eq!(serde_json::to_value(&update).unwrap(), wire);
assert_eq!(
serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
update
);
assert!(update.capabilities.is_undefined());
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(feature = "unstable_subagents")]
#[test]
fn test_subagent_malformed_optional_state() {
use serde_json::json;
for malformed in [json!("running"), json!({}), json!({"state": 4})] {
assert!(serde_json::from_value::<StateUpdate>(malformed.clone()).is_err());
let update: SubagentUpdate = serde_json::from_value(json!({
"sessionId": "child", "state": malformed
}))
.unwrap();
assert!(update.state.is_undefined());
}
let bad_reason: StateUpdate =
serde_json::from_value(json!({"state": "idle", "stopReason": "not_a_reason"})).unwrap();
assert_eq!(bad_reason, StateUpdate::Idle(IdleStateUpdate::new()));
let null_reason: StateUpdate =
serde_json::from_value(json!({"state": "idle", "stopReason": null})).unwrap();
assert_eq!(null_reason, StateUpdate::Idle(IdleStateUpdate::new()));
}
#[cfg(all(feature = "unstable_subagents", feature = "unstable_protocol_v2"))]
#[test]
fn test_subagent_state_v2_wire_parity() {
use serde_json::json;
for wire in [
json!({"state": "running"}),
json!({"state": "idle", "stopReason": "cancelled"}),
json!({"state": "requires_action"}),
json!({"state": "unknown"}),
json!({"state": "unknown", "_meta": {"source": "worker"}}),
json!({"state": "_waiting", "detail": {"ticket": 7}}),
] {
let v1: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
let v2: crate::v2::StateUpdate = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(v1).unwrap(), wire);
assert_eq!(serde_json::to_value(v2).unwrap(), wire);
}
}
#[cfg(feature = "unstable_subagents")]
#[test]
fn test_subagent_capability_semantics() {
use serde_json::json;
let capabilities =
serde_json::to_value(ClientCapabilities::new().subagents(SubagentCapabilities::new()))
.unwrap();
assert_eq!(capabilities["subagents"], json!({}));
let omitted: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
assert!(omitted.subagents.is_none());
let null: ClientCapabilities =
serde_json::from_value(json!({ "subagents": null })).unwrap();
assert!(null.subagents.is_none());
assert_eq!(null, omitted);
let serialized = serde_json::to_value(null).unwrap();
assert_eq!(serialized, serde_json::to_value(omitted).unwrap());
assert!(serialized.get("subagents").is_none());
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
);
let removed = SubagentSessionCapabilities::new();
assert_eq!(serde_json::to_value(&removed).unwrap(), json!({}));
assert!(removed.cancel.is_none());
assert!(enabled.cancel.is_some());
assert!(enabled.cancel(None).cancel.is_none());
let replacement = SubagentUpdate::new("child").capabilities(removed);
assert_eq!(
serde_json::to_value(replacement).unwrap(),
json!({"sessionId": "child", "capabilities": {}})
);
}
#[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
#[test]
fn test_subagent_capability_schema_is_optional_and_nullable() {
use serde_json::json;
let schema = serde_json::to_value(schemars::schema_for!(ClientCapabilities)).unwrap();
let variants = schema["properties"]["subagents"]["anyOf"]
.as_array()
.expect("subagents must allow the capability object or null");
assert!(variants.contains(&json!({"type": "null"})));
assert!(
!schema["required"]
.as_array()
.is_some_and(|required| required.contains(&json!("subagents")))
);
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")))
);
}
#[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(CreateElicitationRequest::new(
crate::v1::ElicitationFormMode::new(
crate::v1::ElicitationSessionScope::new("sess_1"),
crate::v1::ElicitationSchema::new(),
),
"Choose a value",
));
assert_eq!(request.method(), "elicitation/create");
let method = Arc::from(request.method());
let request = crate::v1::JsonRpcMessage::wrap(crate::v1::Request {
id: crate::v1::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(
CompleteElicitationNotification::new("elic_1"),
);
assert_eq!(notification.method(), "elicitation/complete");
let method = Arc::from(notification.method());
let notification = crate::v1::JsonRpcMessage::wrap(crate::v1::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_default_on_malformed_values() {
use serde_json::json;
let capabilities: ClientCapabilities = serde_json::from_value(json!({
"fs": {
"readTextFile": "yes",
"writeTextFile": true
},
"terminal": {}
}))
.unwrap();
assert!(!capabilities.fs.read_text_file);
assert!(capabilities.fs.write_text_file);
assert!(!capabilities.terminal);
let capabilities: ClientCapabilities = serde_json::from_value(json!({
"fs": false
}))
.unwrap();
assert_eq!(capabilities.fs, FileSystemCapabilities::default());
{
let capabilities: ClientCapabilities = serde_json::from_value(json!({
"auth": false
}))
.unwrap();
assert_eq!(capabilities.auth, AuthCapabilities::default());
let capabilities: AuthCapabilities = serde_json::from_value(json!({
"terminal": {}
}))
.unwrap();
assert!(!capabilities.terminal);
}
}
#[test]
fn test_serialization_behavior() {
use serde_json::json;
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
SessionInfoUpdate {
title: MaybeUndefined::Undefined,
updated_at: MaybeUndefined::Undefined,
meta: None
}
);
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
.unwrap(),
SessionInfoUpdate {
title: MaybeUndefined::Null,
updated_at: MaybeUndefined::Null,
meta: None
}
);
assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(
json!({"title": "title", "updatedAt": "timestamp"})
)
.unwrap(),
SessionInfoUpdate {
title: MaybeUndefined::Value("title".to_string()),
updated_at: MaybeUndefined::Value("timestamp".to_string()),
meta: None
}
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
json!({})
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
json!({"title": "title"})
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
json!({"title": null})
);
assert_eq!(
serde_json::to_value(
SessionInfoUpdate::new()
.title("title")
.title(MaybeUndefined::Undefined)
)
.unwrap(),
json!({})
);
}
#[test]
fn test_content_chunk_message_id_serialization() {
use serde_json::json;
assert_eq!(
serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
ContentBlock::Text(crate::v1::TextContent::new("Hello"))
)))
.unwrap(),
json!({
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "Hello"
}
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::AgentMessageChunk(
ContentChunk::new(ContentBlock::Text(crate::v1::TextContent::new("Hello")))
.message_id("msg_agent_c42b9")
))
.unwrap(),
json!({
"sessionUpdate": "agent_message_chunk",
"messageId": "msg_agent_c42b9",
"content": {
"type": "text",
"text": "Hello"
}
})
);
let SessionUpdate::AgentMessageChunk(chunk) = serde_json::from_value(json!({
"sessionUpdate": "agent_message_chunk",
"messageId": null,
"content": {
"type": "text",
"text": "Hello"
}
}))
.unwrap() else {
panic!("expected agent message chunk");
};
assert_eq!(chunk.message_id, None);
}
#[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);
}
#[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"]));
}
#[test]
fn test_client_capabilities_boolean_config_options_serialization() {
use serde_json::json;
let capabilities = ClientCapabilities::new().session(
ClientSessionCapabilities::new().config_options(
SessionConfigOptionsCapabilities::new()
.boolean(BooleanConfigOptionCapabilities::new()),
),
);
let json = serde_json::to_value(&capabilities).unwrap();
assert_eq!(json["session"]["configOptions"]["boolean"], json!({}));
let omitted: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
assert!(omitted.session.is_none());
let null_session: ClientCapabilities = serde_json::from_value(json!({
"session": null
}))
.unwrap();
assert!(null_session.session.is_none());
let null_config_options: ClientCapabilities = serde_json::from_value(json!({
"session": {
"configOptions": null
}
}))
.unwrap();
assert!(
null_config_options
.session
.and_then(|session| session.config_options)
.is_none()
);
let null_boolean: ClientCapabilities = serde_json::from_value(json!({
"session": {
"configOptions": {
"boolean": null
}
}
}))
.unwrap();
assert!(
null_boolean
.session
.and_then(|session| session.config_options)
.and_then(|config_options| config_options.boolean)
.is_none()
);
}
#[cfg(feature = "unstable_plan_operations")]
#[test]
fn test_plan_operations_serialization() {
use serde_json::json;
use crate::v1::{PlanEntry, PlanEntryPriority, PlanEntryStatus, PlanUpdateContent};
let plan_update = SessionUpdate::PlanUpdate(PlanUpdate::new(PlanUpdateContent::items(
"plan-1",
vec![PlanEntry::new(
"Step 1",
PlanEntryPriority::High,
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"
}
]
}
})
);
assert_eq!(
serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
json!({
"sessionUpdate": "plan_removed",
"planId": "plan-1"
})
);
let capabilities = ClientCapabilities::new().plan(PlanCapabilities::new());
let json = serde_json::to_value(&capabilities).unwrap();
assert_eq!(json["plan"], json!({}));
assert_eq!(
serde_json::from_value::<ClientCapabilities>(json!({ "plan": null }))
.unwrap()
.plan,
None
);
}
#[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(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 request_permission_request_rejects_malformed_options() {
use serde_json::json;
assert!(
serde_json::from_value::<RequestPermissionRequest>(json!({
"sessionId": "sess-1",
"toolCall": {"toolCallId": "tc-1"},
"options": "not-an-array"
}))
.is_err()
);
assert!(
serde_json::from_value::<RequestPermissionRequest>(json!({
"sessionId": "sess-1",
"toolCall": {"toolCallId": "tc-1"},
"options": [{"optionId": "allow"}]
}))
.is_err()
);
}
}