Skip to main content

agent_client_protocol_schema/v2/
client.rs

1//! Methods and notifications the client handles/receives.
2//!
3//! This module defines the Client trait and all associated types for implementing
4//! a client that interacts with AI coding agents via the Agent Client Protocol (ACP).
5
6use std::{collections::BTreeMap, sync::Arc};
7
8use derive_more::{Display, From};
9#[cfg(feature = "schemars")]
10use schemars::Schema;
11use serde::{Deserialize, Serialize};
12use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
13
14#[cfg(feature = "unstable_plan_operations")]
15use super::PlanRemoved;
16#[cfg(feature = "unstable_end_turn_token_usage")]
17use super::Usage;
18use super::{
19    AbsolutePath, ContentBlock, ExtNotification, ExtRequest, ExtResponse, Meta, PlanUpdate,
20    SessionConfigOption, SessionId, StopReason, TerminalId, TerminalOutputChunk, TerminalUpdate,
21    ToolCallContentChunk, ToolCallId, ToolCallUpdate,
22};
23use super::{
24    CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse,
25    ElicitationCapabilities,
26};
27use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined};
28
29#[cfg(feature = "unstable_mcp_over_acp")]
30use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse};
31
32// Session updates
33
34/// Notification containing a session update from the agent.
35///
36/// Agents can send session updates at any point while the session exists.
37///
38/// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#3-agent-reports-output)
39#[serde_as]
40#[skip_serializing_none]
41#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION)))]
44#[serde(rename_all = "camelCase")]
45#[non_exhaustive]
46pub struct UpdateSessionNotification {
47    /// The ID of the session this update pertains to.
48    pub session_id: SessionId,
49    /// The actual update content.
50    pub update: SessionUpdate,
51    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
52    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
53    /// Implementations MUST NOT make assumptions about values at these keys.
54    ///
55    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
56    #[serde_as(deserialize_as = "DefaultOnError")]
57    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
58    #[serde(default)]
59    #[serde(rename = "_meta")]
60    pub meta: Option<Meta>,
61}
62
63impl UpdateSessionNotification {
64    /// Builds [`UpdateSessionNotification`] with the required notification fields set; optional fields start unset or empty.
65    #[must_use]
66    pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
67        Self {
68            session_id: session_id.into(),
69            update,
70            meta: None,
71        }
72    }
73
74    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
75    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
76    /// these keys.
77    ///
78    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
79    #[must_use]
80    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
81        self.meta = meta.into_option();
82        self
83    }
84}
85
86/// Different types of updates that can be sent while a session exists.
87///
88/// These updates report messages, progress, and other session activity.
89///
90/// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#3-agent-reports-output)
91#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
94#[non_exhaustive]
95pub enum SessionUpdate {
96    /// A chunk of the user's message being streamed.
97    UserMessageChunk(ContentChunk),
98    /// A user message has been created or updated.
99    ///
100    /// Agents can send this when they accept or replay a user message. When a
101    /// client receives another `user_message` update with the same `messageId`,
102    /// fields in the new update patch the previous fields for that message.
103    UserMessage(UserMessage),
104    /// A chunk of the agent's response being streamed.
105    AgentMessageChunk(ContentChunk),
106    /// An agent message has been created or updated.
107    ///
108    /// Agents can send this in addition to streamed chunks. When a client
109    /// receives another `agent_message` update with the same `messageId`,
110    /// fields in the new update patch the previous fields for that message.
111    AgentMessage(AgentMessage),
112    /// A chunk of the agent's internal reasoning being streamed.
113    AgentThoughtChunk(ContentChunk),
114    /// An agent thought or reasoning message has been created or updated.
115    ///
116    /// Agents can send this in addition to streamed chunks. When a client
117    /// receives another `agent_thought` update with the same `messageId`,
118    /// fields in the new update patch the previous fields for that message.
119    AgentThought(AgentThought),
120    /// A snapshot of the session's aggregate foreground activity.
121    StateUpdate(StateUpdate),
122    /// A chunk of tool-call content being streamed.
123    ToolCallContentChunk(ToolCallContentChunk),
124    /// A tool call has been created or updated.
125    ToolCallUpdate(ToolCallUpdate),
126    /// An agent-owned terminal has been created or updated.
127    TerminalUpdate(TerminalUpdate),
128    /// A chunk of bytes appended to an agent-owned terminal's output.
129    TerminalOutputChunk(TerminalOutputChunk),
130    /// A content update for a plan identified by ID.
131    /// See protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/v2/agent-plan)
132    PlanUpdate(PlanUpdate),
133    /// **UNSTABLE**
134    ///
135    /// This capability is not part of the spec yet, and may be removed or changed at any point.
136    ///
137    /// Removal notice for a plan identified by ID.
138    #[cfg(feature = "unstable_plan_operations")]
139    PlanRemoved(PlanRemoved),
140    /// Available commands are ready or have changed
141    AvailableCommandsUpdate(AvailableCommandsUpdate),
142    /// Session configuration options have been updated.
143    ConfigOptionUpdate(ConfigOptionUpdate),
144    /// Session metadata has been updated (title, timestamps, custom metadata)
145    SessionInfoUpdate(SessionInfoUpdate),
146    /// Context window and cost update for the session.
147    UsageUpdate(UsageUpdate),
148    /// Information for the user that is not part of session history.
149    ///
150    /// No Client capability is required. Clients that do not understand or
151    /// present notices may ignore them.
152    Notice(Notice),
153    /// A context compaction has been created or updated.
154    CompactionUpdate(CompactionUpdate),
155    /// A content block appended to a context compaction's retained summary.
156    CompactionSummaryChunk(CompactionSummaryChunk),
157    /// **UNSTABLE**
158    ///
159    /// This capability is not part of the spec yet, and may be removed or changed at any point.
160    ///
161    /// Announces a child session created and owned by this session, or updates
162    /// that ownership association's metadata.
163    #[cfg(feature = "unstable_subagents")]
164    SubagentUpdate(SubagentUpdate),
165    /// **UNSTABLE**
166    ///
167    /// This capability is not part of the spec yet, and may be removed or changed at any point.
168    ///
169    /// A message upsert observed in this session's transcript, sent to or
170    /// received from another session.
171    #[cfg(feature = "unstable_subagents")]
172    SessionMessage(SessionMessage),
173    /// **UNSTABLE**
174    ///
175    /// This capability is not part of the spec yet, and may be removed or changed at any point.
176    ///
177    /// One content block appended to a sent or received session message.
178    #[cfg(feature = "unstable_subagents")]
179    SessionMessageChunk(SessionMessageChunk),
180    /// Custom or future session update.
181    ///
182    /// Values beginning with `_` are reserved for implementation-specific
183    /// extensions. Unknown values that do not begin with `_` are reserved for
184    /// future ACP variants.
185    ///
186    /// Receivers that do not understand this update type should preserve the
187    /// raw payload when storing, replaying, proxying, or forwarding session
188    /// history, and otherwise ignore it or display it generically.
189    #[serde(untagged)]
190    Other(OtherSessionUpdate),
191}
192
193/// **UNSTABLE**
194///
195/// This capability is not part of the spec yet, and may be removed or changed at any point.
196///
197/// A streamed content block of an inter-session message.
198///
199/// A chunk carries only content. The message's participants and references come from
200/// `session_message` upserts, which can arrive before, between, or after its chunks.
201#[cfg(feature = "unstable_subagents")]
202#[serde_as]
203#[skip_serializing_none]
204#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206#[serde(rename_all = "camelCase")]
207#[non_exhaustive]
208pub struct SessionMessageChunk {
209    /// Identifier of this message within the enclosing session's transcript.
210    pub message_id: MessageId,
211    /// A single content block appended to the message.
212    pub content: ContentBlock,
213    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
214    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
215    /// Implementations MUST NOT make assumptions about values at these keys.
216    /// This metadata is scoped to the chunk and does not patch stored metadata.
217    ///
218    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
219    #[serde_as(deserialize_as = "DefaultOnError")]
220    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
221    #[serde(default, rename = "_meta")]
222    pub meta: Option<Meta>,
223}
224
225#[cfg(feature = "unstable_subagents")]
226impl SessionMessageChunk {
227    /// Builds a single streamed content block without chunk metadata.
228    #[must_use]
229    pub fn new(message_id: impl Into<MessageId>, content: ContentBlock) -> Self {
230        Self {
231            message_id: message_id.into(),
232            content,
233            meta: None,
234        }
235    }
236
237    /// Sets optional chunk-scoped metadata.
238    #[must_use]
239    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
240        self.meta = meta.into_option();
241        self
242    }
243}
244
245/// **UNSTABLE**
246///
247/// This capability is not part of the spec yet, and may be removed or changed at any point.
248///
249/// An upsert for an inter-session message.
250#[cfg(feature = "unstable_subagents")]
251#[serde_as]
252#[skip_serializing_none]
253#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
255#[serde(rename_all = "camelCase")]
256#[non_exhaustive]
257pub struct SessionMessage {
258    /// Identifier of this message within the enclosing session's transcript.
259    pub message_id: MessageId,
260    /// The sending session.
261    ///
262    /// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it.
263    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
264    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
265    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
266    pub sender_session_id: MaybeUndefined<SessionId>,
267    /// The receiving session.
268    ///
269    /// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it.
270    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
271    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
272    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
273    pub recipient_session_id: MaybeUndefined<SessionId>,
274    /// On an incoming view, the `messageId` of the same message's outgoing view in the
275    /// sender's transcript.
276    ///
277    /// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it. It can arrive
278    /// after the message starts streaming.
279    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
280    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
281    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
282    pub sender_message_id: MaybeUndefined<MessageId>,
283    /// On an outgoing view, the `messageId` of the same message's incoming view in the
284    /// recipient's transcript.
285    ///
286    /// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it. It can arrive
287    /// after the message starts streaming.
288    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
289    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
290    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
291    pub recipient_message_id: MaybeUndefined<MessageId>,
292    /// Omitted leaves content unchanged; `null` clears it; a concrete array
293    /// replaces the whole content collection.
294    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
295    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
296    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
297    pub content: MaybeUndefined<Vec<ContentBlock>>,
298    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
299    /// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
300    /// clears it. An object patches the stored metadata by top-level key: keys it omits are
301    /// unchanged, a key set to `null` is removed, and any other value replaces that key's
302    /// stored value without merging nested objects. Beyond this patch rule, implementations
303    /// MUST NOT make assumptions about values at these keys.
304    ///
305    /// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/v2/extensibility#metadata-patches)
306    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
307    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
308    #[serde(
309        default,
310        rename = "_meta",
311        skip_serializing_if = "MaybeUndefined::is_undefined"
312    )]
313    pub meta: MaybeUndefined<Meta>,
314}
315
316#[cfg(feature = "unstable_subagents")]
317impl SessionMessage {
318    /// Builds a message upsert with its transcript-local ID.
319    #[must_use]
320    pub fn new(message_id: impl Into<MessageId>) -> Self {
321        Self {
322            message_id: message_id.into(),
323            sender_session_id: MaybeUndefined::Undefined,
324            recipient_session_id: MaybeUndefined::Undefined,
325            sender_message_id: MaybeUndefined::Undefined,
326            recipient_message_id: MaybeUndefined::Undefined,
327            content: MaybeUndefined::Undefined,
328            meta: MaybeUndefined::Undefined,
329        }
330    }
331
332    /// Sets, clears, or leaves unchanged the sending session.
333    #[must_use]
334    pub fn sender_session_id(
335        mut self,
336        sender_session_id: impl IntoMaybeUndefined<SessionId>,
337    ) -> Self {
338        self.sender_session_id = sender_session_id.into_maybe_undefined();
339        self
340    }
341
342    /// Sets, clears, or leaves unchanged the receiving session.
343    #[must_use]
344    pub fn recipient_session_id(
345        mut self,
346        recipient_session_id: impl IntoMaybeUndefined<SessionId>,
347    ) -> Self {
348        self.recipient_session_id = recipient_session_id.into_maybe_undefined();
349        self
350    }
351
352    /// Links an incoming view to the outgoing view's message ID in the sender's transcript.
353    #[must_use]
354    pub fn sender_message_id(
355        mut self,
356        sender_message_id: impl IntoMaybeUndefined<MessageId>,
357    ) -> Self {
358        self.sender_message_id = sender_message_id.into_maybe_undefined();
359        self
360    }
361
362    /// Links an outgoing view to the incoming view's message ID in the recipient's transcript.
363    #[must_use]
364    pub fn recipient_message_id(
365        mut self,
366        recipient_message_id: impl IntoMaybeUndefined<MessageId>,
367    ) -> Self {
368        self.recipient_message_id = recipient_message_id.into_maybe_undefined();
369        self
370    }
371
372    /// Replaces, clears, or leaves unchanged the message content.
373    #[must_use]
374    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
375        self.content = content.into_maybe_undefined();
376        self
377    }
378
379    /// Patches, clears, or leaves unchanged message metadata.
380    ///
381    /// A concrete object patches stored metadata by top-level key; see
382    /// [`apply_meta_update`](super::apply_meta_update).
383    #[must_use]
384    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
385        self.meta = meta.into_maybe_undefined();
386        self
387    }
388}
389
390#[cfg(all(test, feature = "unstable_subagents"))]
391mod session_message_tests {
392    use super::*;
393    use serde_json::{Value, json};
394
395    #[test]
396    fn transcript_envelopes_and_multimodal_content() {
397        for (transcript, sender, recipient, id) in [
398            ("parent", "parent", "child", "sent-1"),
399            ("child", "parent", "child", "received-9"),
400            ("child", "child", "parent", "sent-2"),
401        ] {
402            let wire = json!({"sessionId": transcript, "update": {
403                "sessionUpdate": "session_message", "messageId": id,
404                "senderSessionId": sender, "recipientSessionId": recipient, "content": [
405                    {"type": "text", "text": "hello"},
406                    {"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"}
407                ]
408            }});
409            let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
410            assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
411        }
412    }
413
414    #[test]
415    fn chunks_and_upserts_share_transcript_local_identity() {
416        for (transcript, id) in [("parent", "sent-1"), ("child", "received-9")] {
417            let wire = json!({"sessionId": transcript, "update": {
418                "sessionUpdate": "session_message_chunk", "messageId": id,
419                "content": {"type": "text", "text": "hello"}
420            }});
421            let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
422            assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
423            let upsert = SessionMessage::new(id)
424                .sender_session_id(SessionId::new("parent"))
425                .recipient_session_id(SessionId::new("child"))
426                .content(vec![]);
427            let chunk = SessionMessageChunk::new(
428                id,
429                ContentBlock::Text(crate::v2::TextContent::new("hello")),
430            );
431            assert_eq!(chunk.message_id, upsert.message_id);
432            assert_eq!(
433                serde_json::to_value(SessionUpdate::SessionMessageChunk(chunk)).unwrap(),
434                wire["update"]
435            );
436            assert_eq!(serde_json::to_value(upsert).unwrap()["content"], json!([]));
437        }
438        // A chunk carries only content, so identity fields on one are not part of it.
439        let with_identity = json!({"sessionUpdate": "session_message_chunk", "messageId": "m1",
440            "senderSessionId": "parent", "recipientSessionId": "child",
441            "senderMessageId": "sent-1", "recipientMessageId": "received-1",
442            "content": {"type": "text", "text": "hello"}});
443        let decoded: SessionUpdate = serde_json::from_value(with_identity).unwrap();
444        assert_eq!(
445            serde_json::to_value(decoded).unwrap(),
446            json!({"sessionUpdate": "session_message_chunk", "messageId": "m1",
447                "content": {"type": "text", "text": "hello"}})
448        );
449    }
450
451    #[test]
452    fn required_ids_and_chunk_content_with_upsert_patch_semantics() {
453        for (kind, content) in [
454            ("session_message", json!([])),
455            (
456                "session_message_chunk",
457                json!({"type": "text", "text": "hello"}),
458            ),
459        ] {
460            let mut base = json!({"sessionUpdate": kind, "messageId": "m1", "content": content});
461            if kind == "session_message" {
462                base["senderSessionId"] = json!("parent");
463                base["recipientSessionId"] = json!("child");
464            }
465            {
466                let key = "messageId";
467                let mut missing = base.clone();
468                missing.as_object_mut().unwrap().remove(key);
469                assert!(
470                    serde_json::from_value::<SessionUpdate>(missing).is_err(),
471                    "{kind} {key}"
472                );
473                let mut null = base.clone();
474                null[key] = Value::Null;
475                assert!(
476                    serde_json::from_value::<SessionUpdate>(null).is_err(),
477                    "{kind} {key}"
478                );
479                let mut non_string = base.clone();
480                non_string[key] = json!(42);
481                assert!(
482                    serde_json::from_value::<SessionUpdate>(non_string).is_err(),
483                    "{kind} {key}"
484                );
485            }
486            if kind == "session_message_chunk" {
487                for content in [None, Some(Value::Null), Some(json!([]))] {
488                    let mut invalid = base.clone();
489                    invalid.as_object_mut().unwrap().remove("content");
490                    if let Some(content) = content {
491                        invalid["content"] = content;
492                    }
493                    assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
494                }
495            } else {
496                for content in [None, Some(Value::Null), Some(json!([])), Some(json!({}))] {
497                    let mut wire = base.clone();
498                    wire.as_object_mut().unwrap().remove("content");
499                    if let Some(content) = content {
500                        wire["content"] = content;
501                    }
502                    let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
503                    let encoded = serde_json::to_value(decoded).unwrap();
504                    if wire["content"].is_object() {
505                        wire.as_object_mut().unwrap().remove("content");
506                    }
507                    assert_eq!(encoded, wire);
508                }
509            }
510            for meta in [None, Some(Value::Null), Some(json!({"tag": "value"}))] {
511                let mut wire = base.clone();
512                if let Some(meta) = meta {
513                    wire["_meta"] = meta;
514                }
515                let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
516                let encoded = serde_json::to_value(decoded).unwrap();
517                if kind == "session_message" {
518                    assert_eq!(encoded, wire);
519                } else if wire["_meta"].is_null() {
520                    assert_eq!(encoded, base);
521                } else {
522                    assert_eq!(encoded, wire);
523                }
524            }
525        }
526        let metadata_only = json!({"sessionUpdate": "session_message",
527            "messageId": "m1", "senderSessionId": "parent",
528            "recipientSessionId": "child", "_meta": {"tag": "value"}});
529        let decoded: SessionUpdate = serde_json::from_value(metadata_only.clone()).unwrap();
530        assert_eq!(serde_json::to_value(decoded).unwrap(), metadata_only);
531        let cleared = SessionMessage::new("m1")
532            .sender_session_id(SessionId::new("parent"))
533            .recipient_session_id(SessionId::new("child"))
534            .content(None)
535            .meta(None);
536        assert_eq!(
537            serde_json::to_value(cleared).unwrap(),
538            json!({"messageId": "m1", "senderSessionId": "parent",
539                "recipientSessionId": "child", "content": null, "_meta": null})
540        );
541    }
542
543    #[cfg(feature = "schemars")]
544    #[test]
545    fn schemas_require_ids_and_chunk_content_but_not_upsert_content() {
546        let upsert = serde_json::to_value(schemars::schema_for!(SessionMessage)).unwrap();
547        let required = upsert["required"].as_array().unwrap();
548        assert_eq!(required, &vec![json!("messageId")]);
549        let chunk = serde_json::to_value(schemars::schema_for!(SessionMessageChunk)).unwrap();
550        let required = chunk["required"].as_array().unwrap();
551        assert_eq!(required.len(), 2);
552        assert!(required.contains(&json!("messageId")));
553        assert!(required.contains(&json!("content")));
554    }
555
556    #[test]
557    fn endpoints_can_arrive_late_or_be_omitted_from_later_events() {
558        let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
559        let minimal = SessionMessage::new("m1");
560        let first = SessionMessageChunk::new("m1", block.clone());
561        assert_eq!(
562            serde_json::to_value(&minimal).unwrap(),
563            json!({"messageId": "m1"})
564        );
565        assert_eq!(
566            serde_json::to_value(&first).unwrap(),
567            json!({"messageId": "m1", "content": {"type": "text", "text": "hello"}})
568        );
569        let enriched = SessionMessage::new("m1")
570            .sender_session_id(SessionId::new("parent"))
571            .recipient_session_id(SessionId::new("child"));
572        assert_eq!(
573            enriched.sender_session_id,
574            MaybeUndefined::Value(SessionId::new("parent"))
575        );
576        assert_eq!(
577            enriched.recipient_session_id,
578            MaybeUndefined::Value(SessionId::new("child"))
579        );
580        let later = SessionMessage::new("m1").sender_session_id(SessionId::new("parent"));
581        assert_eq!(
582            later.sender_session_id,
583            MaybeUndefined::Value(SessionId::new("parent"))
584        );
585        assert_eq!(later.recipient_session_id, MaybeUndefined::Undefined);
586        let more = SessionMessageChunk::new("m1", block);
587        for update in [
588            SessionUpdate::SessionMessage(minimal),
589            SessionUpdate::SessionMessageChunk(first),
590            SessionUpdate::SessionMessage(enriched),
591            SessionUpdate::SessionMessage(later),
592            SessionUpdate::SessionMessageChunk(more),
593        ] {
594            let wire = serde_json::to_value(&update).unwrap();
595            let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
596            assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
597        }
598    }
599
600    #[test]
601    fn null_endpoints_clear_invalid_are_absent_but_message_id_is_required() {
602        let mut base = json!({"sessionUpdate": "session_message", "messageId": "m1",
603            "senderSessionId": null, "recipientSessionId": 42});
604        let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap();
605        let encoded = serde_json::to_value(decoded).unwrap();
606        base.as_object_mut().unwrap().remove("recipientSessionId");
607        assert_eq!(encoded, base);
608        for bad in [Value::Null, json!(42)] {
609            let mut invalid = base.clone();
610            invalid["messageId"] = bad;
611            assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
612        }
613    }
614
615    #[test]
616    fn message_references_link_views_and_can_arrive_late() {
617        let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
618        let incoming = SessionMessage::new("received-1")
619            .sender_session_id(SessionId::new("parent"))
620            .recipient_session_id(SessionId::new("child"))
621            .sender_message_id(MessageId::new("sent-1"))
622            .content(vec![block]);
623        // The outgoing view gets its reference after it started, with a metadata-only upsert.
624        let outgoing =
625            SessionMessage::new("sent-1").recipient_message_id(MessageId::new("received-1"));
626        for (update, wire) in [
627            (
628                SessionUpdate::SessionMessage(incoming),
629                json!({"sessionUpdate": "session_message", "messageId": "received-1",
630                    "senderSessionId": "parent", "recipientSessionId": "child",
631                    "senderMessageId": "sent-1", "content": [{"type": "text", "text": "hello"}]}),
632            ),
633            (
634                SessionUpdate::SessionMessage(outgoing),
635                json!({"sessionUpdate": "session_message", "messageId": "sent-1",
636                    "recipientMessageId": "received-1"}),
637            ),
638        ] {
639            assert_eq!(serde_json::to_value(&update).unwrap(), wire);
640            let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
641            assert_eq!(decoded, update);
642        }
643
644        let mut wire = json!({"sessionUpdate": "session_message", "messageId": "m1",
645            "senderMessageId": null, "recipientMessageId": 42});
646        let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
647        wire.as_object_mut().unwrap().remove("recipientMessageId");
648        assert_eq!(
649            serde_json::to_value(decoded).unwrap(),
650            wire,
651            "null clears a reference and a malformed one is not supplied"
652        );
653    }
654}
655
656#[cfg(all(test, not(feature = "unstable_subagents")))]
657mod disabled_session_message_tests {
658    use super::*;
659    use serde_json::json;
660
661    #[test]
662    fn preserves_both_unknown_message_discriminators() {
663        for (kind, content) in [
664            ("session_message", json!([])),
665            (
666                "session_message_chunk",
667                json!({"type": "text", "text": "hello"}),
668            ),
669        ] {
670            let wire = json!({"sessionUpdate": kind, "messageId": "m1",
671                "senderSessionId": "parent", "recipientSessionId": "child", "content": content});
672            let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
673            assert!(matches!(decoded, SessionUpdate::Other(_)));
674            assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
675        }
676    }
677}
678
679/// Severity hint for a session notice.
680#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
681#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
682#[serde(rename_all = "snake_case")]
683#[non_exhaustive]
684pub enum NoticeSeverity {
685    /// Informational notice.
686    Info,
687    /// Warning notice.
688    Warning,
689    /// Error notice.
690    Error,
691    /// Custom or future notice severity.
692    ///
693    /// Values beginning with `_` are reserved for implementation-specific
694    /// extensions. Other unknown values are reserved for future ACP severities.
695    #[serde(untagged)]
696    Other(String),
697}
698
699/// Fire-and-forget information for the user.
700///
701/// Notices are live events rather than session history. Agents must not rely on
702/// a notice being received, displayed, or seen by the user.
703/// No Client capability is required, and unsupported Clients may ignore notices.
704///
705/// See protocol docs: [Session Notices](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#session-notices)
706#[serde_as]
707#[skip_serializing_none]
708#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
709#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
710#[serde(rename_all = "camelCase")]
711#[non_exhaustive]
712pub struct Notice {
713    /// Presentation severity hint.
714    pub severity: NoticeSeverity,
715    /// Required non-empty plain-text title that can stand alone.
716    #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
717    pub title: String,
718    /// Optional plain-text detail or guidance.
719    ///
720    /// Omitted and `null` are equivalent and mean no description was supplied.
721    #[serde_as(deserialize_as = "DefaultOnError")]
722    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
723    #[serde(default)]
724    pub description: Option<String>,
725    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
726    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
727    /// Implementations MUST NOT make assumptions about values at these keys.
728    ///
729    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
730    #[serde_as(deserialize_as = "DefaultOnError")]
731    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
732    #[serde(default, rename = "_meta")]
733    pub meta: Option<Meta>,
734}
735
736impl Notice {
737    /// Builds a notice with the required fields set and optional fields omitted.
738    #[must_use]
739    pub fn new(severity: NoticeSeverity, title: impl Into<String>) -> Self {
740        Self {
741            severity,
742            title: title.into(),
743            description: None,
744            meta: None,
745        }
746    }
747
748    /// Sets or clears the optional description.
749    #[must_use]
750    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
751        self.description = description.into_option();
752        self
753    }
754
755    /// Sets or clears notice-scoped metadata.
756    #[must_use]
757    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
758        self.meta = meta.into_option();
759        self
760    }
761}
762
763/// Unique identifier for a context compaction within a session.
764#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
765#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
766#[serde(transparent)]
767#[from(forward)]
768#[non_exhaustive]
769pub struct CompactionId(pub Arc<str>);
770
771impl CompactionId {
772    /// Wraps a protocol string as a typed [`CompactionId`].
773    #[must_use]
774    pub fn new(id: impl Into<Self>) -> Self {
775        id.into()
776    }
777}
778
779/// Lifecycle state of a context compaction.
780#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
781#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
782#[serde(rename_all = "snake_case")]
783#[non_exhaustive]
784pub enum CompactionStatus {
785    /// Compaction has started and has not finished.
786    #[default]
787    InProgress,
788    /// Compaction finished successfully.
789    Completed,
790    /// Compaction finished unsuccessfully.
791    Failed,
792    /// Compaction was cancelled before it finished.
793    Cancelled,
794    /// Custom or future compaction status.
795    ///
796    /// Values beginning with `_` are reserved for implementation-specific
797    /// extensions. Other unknown values are reserved for future ACP statuses.
798    #[serde(untagged)]
799    Other(String),
800}
801
802/// A context compaction upsert. The first notification fixes the compaction's
803/// timeline position. Later updates with the same ID patch that entity in place.
804///
805/// `summary`, `error`, and `_meta` have patch semantics: omission leaves the
806/// stored value unchanged, `null` clears it, and a concrete value replaces it.
807/// `summary: []` also clears the summary. A concrete `_meta` object patches the
808/// stored metadata by top-level key instead of replacing it.
809#[serde_as]
810#[skip_serializing_none]
811#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
812#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
813#[serde(rename_all = "camelCase")]
814#[non_exhaustive]
815pub struct CompactionUpdate {
816    /// The Agent-owned ID of this compaction, unique within the session.
817    pub compaction_id: CompactionId,
818    /// Current lifecycle status.
819    pub status: CompactionStatus,
820    /// Complete replacement user-displayable summary content for the compaction.
821    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
822    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
823    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
824    pub summary: MaybeUndefined<Vec<ContentBlock>>,
825    /// Human-readable error details for the compaction.
826    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
827    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
828    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
829    pub error: MaybeUndefined<String>,
830    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
831    /// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
832    /// clears it. An object patches the stored metadata by top-level key: keys it omits are
833    /// unchanged, a key set to `null` is removed, and any other value replaces that key's
834    /// stored value without merging nested objects. Beyond this patch rule, implementations
835    /// MUST NOT make assumptions about values at these keys.
836    ///
837    /// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/v2/extensibility#metadata-patches)
838    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
839    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
840    #[serde(
841        rename = "_meta",
842        default,
843        skip_serializing_if = "MaybeUndefined::is_undefined"
844    )]
845    pub meta: MaybeUndefined<Meta>,
846}
847
848impl CompactionUpdate {
849    /// Builds a compaction update with optional patch fields omitted.
850    #[must_use]
851    pub fn new(compaction_id: impl Into<CompactionId>, status: CompactionStatus) -> Self {
852        Self {
853            compaction_id: compaction_id.into(),
854            status,
855            summary: MaybeUndefined::Undefined,
856            error: MaybeUndefined::Undefined,
857            meta: MaybeUndefined::Undefined,
858        }
859    }
860
861    /// Sets, clears, or omits the complete retained summary patch.
862    #[must_use]
863    pub fn summary(mut self, summary: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
864        self.summary = summary.into_maybe_undefined();
865        self
866    }
867
868    /// Sets, clears, or omits the error details patch.
869    #[must_use]
870    pub fn error(mut self, error: impl IntoMaybeUndefined<String>) -> Self {
871        self.error = error.into_maybe_undefined();
872        self
873    }
874
875    /// Patches, clears, or leaves unchanged compaction metadata.
876    ///
877    /// A concrete object patches stored metadata by top-level key; see
878    /// [`apply_meta_update`](super::apply_meta_update).
879    #[must_use]
880    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
881        self.meta = meta.into_maybe_undefined();
882        self
883    }
884}
885
886/// A content block appended to a compaction's summary. A first-seen ID creates
887/// an in-progress compaction. Chunks append in receive order.
888#[serde_as]
889#[skip_serializing_none]
890#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
891#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
892#[serde(rename_all = "camelCase")]
893#[non_exhaustive]
894pub struct CompactionSummaryChunk {
895    /// ID of the compaction whose summary receives this content.
896    pub compaction_id: CompactionId,
897    /// One content block to append.
898    pub content: ContentBlock,
899    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
900    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
901    /// Implementations MUST NOT make assumptions about values at these keys.
902    /// This metadata is scoped to the chunk and does not patch stored metadata.
903    ///
904    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
905    #[serde_as(deserialize_as = "DefaultOnError")]
906    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
907    #[serde(default, rename = "_meta")]
908    pub meta: Option<Meta>,
909}
910
911impl CompactionSummaryChunk {
912    /// Builds a summary chunk without metadata.
913    #[must_use]
914    pub fn new(compaction_id: impl Into<CompactionId>, content: ContentBlock) -> Self {
915        Self {
916            compaction_id: compaction_id.into(),
917            content,
918            meta: None,
919        }
920    }
921
922    /// Sets or clears chunk-scoped metadata.
923    #[must_use]
924    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
925        self.meta = meta.into_option();
926        self
927    }
928}
929
930/// **UNSTABLE**
931///
932/// This capability is not part of the spec yet, and may be removed or changed at any point.
933///
934/// Notification that the enclosing parent session created and owns a child session.
935///
936/// Later updates modify the existing association's metadata, not its ownership.
937///
938/// Sent on the immediate parent session. The first update for an unknown
939/// `sessionId` announces the child and MUST be sent
940/// before any live request or notification bearing the child's session ID,
941/// including messages naming it as sender or recipient. Child events are
942/// delivered automatically; no separate child load, resume, or subscription is needed.
943/// Understanding this update, registering child sessions, and applying their
944/// operation restrictions are baseline v2 requirements; no Client capability
945/// is required.
946///
947/// Only `sessionId` is required. Other fields have
948/// patch semantics: omitted fields leave the stored value unchanged, `null`
949/// clears or unsets the value, and concrete values replace it. A concrete
950/// `_meta` object instead patches the stored metadata by top-level key. For
951/// `state`, `null` removes the current state report and leaves activity
952/// unconfirmed; it does not report idle or cancellation. A child whose capabilities are
953/// unset permits no Client-initiated session mutations.
954///
955/// The title and description provide the parent's display metadata for the
956/// child. Agents SHOULD mirror known child state changes in the parent's
957/// `state` using the same `StateUpdate` snapshot as the
958/// ordinary `state_update` notification on the child session. This reports
959/// child state, not a separate parent-owned lifecycle. Consumers should
960/// treat duplicate reports idempotently.
961/// Completing or cancelling work does not end the association: the parent may
962/// message the same child again. Individual messages and their outcomes do not
963/// change this association.
964#[cfg(feature = "unstable_subagents")]
965#[serde_as]
966#[skip_serializing_none]
967#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
968#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
969#[serde(rename_all = "camelCase")]
970#[non_exhaustive]
971pub struct SubagentUpdate {
972    /// The opaque session ID identifying the child in all ACP messages.
973    pub session_id: SessionId,
974    /// The parent's human-readable display title for this child. It need not be unique.
975    ///
976    /// Optional and nullable. Omitted means unchanged; `null` clears it. If no
977    /// title is set, the Client chooses a fallback presentation.
978    #[serde_as(deserialize_as = "DefaultOnError")]
979    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
980    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
981    pub title: MaybeUndefined<String>,
982    /// The parent's human-readable description of the child's role or purpose.
983    ///
984    /// Optional and nullable. Omitted means unchanged; `null` clears it. This is
985    /// current display metadata, not the history of instructions sent to the child.
986    #[serde_as(deserialize_as = "DefaultOnError")]
987    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
988    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
989    pub description: MaybeUndefined<String>,
990    /// Client-initiated session mutations permitted for this subagent session.
991    ///
992    /// Read-only operations retain their normal protocol semantics and
993    /// capability requirements.
994    #[serde_as(deserialize_as = "DefaultOnError")]
995    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
996    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
997    pub capabilities: MaybeUndefined<SubagentSessionCapabilities>,
998    /// The child's current foreground state, mirrored onto its parent association.
999    ///
1000    /// Optional and nullable. Omitted means unchanged; `null` removes the
1001    /// current report and leaves activity unconfirmed (not idle or cancelled).
1002    /// A concrete `StateUpdate` replaces the entire previous snapshot.
1003    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1004    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1005    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1006    pub state: MaybeUndefined<StateUpdate>,
1007    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1008    /// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
1009    /// clears it. An object patches the stored metadata by top-level key: keys it omits are
1010    /// unchanged, a key set to `null` is removed, and any other value replaces that key's
1011    /// stored value without merging nested objects. Beyond this patch rule, implementations
1012    /// MUST NOT make assumptions about values at these keys.
1013    ///
1014    /// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/v2/extensibility#metadata-patches)
1015    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1016    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1017    #[serde(
1018        rename = "_meta",
1019        default,
1020        skip_serializing_if = "MaybeUndefined::is_undefined"
1021    )]
1022    pub meta: MaybeUndefined<Meta>,
1023}
1024
1025#[cfg(feature = "unstable_subagents")]
1026impl SubagentUpdate {
1027    /// Builds a subagent upsert with only its required session ID set.
1028    #[must_use]
1029    pub fn new(session_id: impl Into<SessionId>) -> Self {
1030        Self {
1031            session_id: session_id.into(),
1032            title: MaybeUndefined::Undefined,
1033            description: MaybeUndefined::Undefined,
1034            capabilities: MaybeUndefined::Undefined,
1035            state: MaybeUndefined::Undefined,
1036            meta: MaybeUndefined::Undefined,
1037        }
1038    }
1039
1040    /// Sets, clears, or leaves unchanged the parent's display title for the child.
1041    #[must_use]
1042    pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1043        self.title = title.into_maybe_undefined();
1044        self
1045    }
1046
1047    /// Sets, clears, or leaves unchanged the parent's description of the child.
1048    #[must_use]
1049    pub fn description(mut self, description: impl IntoMaybeUndefined<String>) -> Self {
1050        self.description = description.into_maybe_undefined();
1051        self
1052    }
1053
1054    /// Sets, clears, or leaves unchanged the permitted client-initiated session mutations.
1055    #[must_use]
1056    pub fn capabilities(
1057        mut self,
1058        capabilities: impl IntoMaybeUndefined<SubagentSessionCapabilities>,
1059    ) -> Self {
1060        self.capabilities = capabilities.into_maybe_undefined();
1061        self
1062    }
1063
1064    /// Replaces, clears, or leaves unchanged the child's mirrored state snapshot.
1065    #[must_use]
1066    pub fn state(mut self, state: impl IntoMaybeUndefined<StateUpdate>) -> Self {
1067        self.state = state.into_maybe_undefined();
1068        self
1069    }
1070
1071    /// Patches, clears, or leaves unchanged subagent metadata.
1072    ///
1073    /// A concrete object patches stored metadata by top-level key; see
1074    /// [`apply_meta_update`](super::apply_meta_update).
1075    #[must_use]
1076    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1077        self.meta = meta.into_maybe_undefined();
1078        self
1079    }
1080}
1081
1082/// **UNSTABLE**
1083///
1084/// This capability is not part of the spec yet, and may be removed or changed at any point.
1085///
1086/// Client-initiated session mutations permitted for a specific subagent session.
1087///
1088/// A mutation requires an explicit per-child capability; support for the method
1089/// on ordinary sessions does not grant support on a child.
1090#[cfg(feature = "unstable_subagents")]
1091#[serde_as]
1092#[skip_serializing_none]
1093#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1094#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1095#[serde(rename_all = "camelCase")]
1096#[non_exhaustive]
1097pub struct SubagentSessionCapabilities {
1098    /// Permits the client to cancel this child's current work without ending
1099    /// the session. Omitted and `null` are equivalent and mean unsupported; an object (including
1100    /// `{}`) means supported.
1101    #[serde_as(deserialize_as = "DefaultOnError")]
1102    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1103    #[serde(default)]
1104    pub cancel: Option<SessionCancelCapabilities>,
1105    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1106    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1107    /// Implementations MUST NOT make assumptions about values at these keys.
1108    ///
1109    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1110    #[serde_as(deserialize_as = "DefaultOnError")]
1111    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1112    #[serde(default)]
1113    #[serde(rename = "_meta")]
1114    pub meta: Option<Meta>,
1115}
1116
1117#[cfg(feature = "unstable_subagents")]
1118impl SubagentSessionCapabilities {
1119    /// Builds an empty capability set; cancellation is disabled.
1120    #[must_use]
1121    pub fn new() -> Self {
1122        Self::default()
1123    }
1124
1125    /// Sets or removes permission to cancel this child's current work.
1126    #[must_use]
1127    pub fn cancel(mut self, cancel: impl IntoOption<SessionCancelCapabilities>) -> Self {
1128        self.cancel = cancel.into_option();
1129        self
1130    }
1131
1132    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1133    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1134    /// these keys.
1135    #[must_use]
1136    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1137        self.meta = meta.into_option();
1138        self
1139    }
1140}
1141
1142/// **UNSTABLE**
1143///
1144/// This capability is not part of the spec yet, and may be removed or changed at any point.
1145///
1146/// Capability to cancel work in a subagent session without ending that session.
1147///
1148/// Supplying `{}` advertises support; an omitted or `null` `cancel` does not.
1149#[cfg(feature = "unstable_subagents")]
1150#[serde_as]
1151#[skip_serializing_none]
1152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1153#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1154#[non_exhaustive]
1155pub struct SessionCancelCapabilities {
1156    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1157    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1158    /// Implementations MUST NOT make assumptions about values at these keys.
1159    ///
1160    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1161    #[serde_as(deserialize_as = "DefaultOnError")]
1162    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1163    #[serde(default)]
1164    #[serde(rename = "_meta")]
1165    pub meta: Option<Meta>,
1166}
1167
1168#[cfg(feature = "unstable_subagents")]
1169impl SessionCancelCapabilities {
1170    /// Builds an empty capability object advertising cancellation support.
1171    #[must_use]
1172    pub fn new() -> Self {
1173        Self::default()
1174    }
1175
1176    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1177    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1178    /// these keys.
1179    ///
1180    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1181    #[must_use]
1182    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1183        self.meta = meta.into_option();
1184        self
1185    }
1186}
1187
1188/// Custom or future session update payload.
1189///
1190/// This preserves the unknown `sessionUpdate` discriminator and the rest of the
1191/// update object for clients that store, replay, proxy, or forward session
1192/// history.
1193#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1194#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1195#[cfg_attr(feature = "schemars", schemars(inline))]
1196#[cfg_attr(feature = "schemars", schemars(transform = other_session_update_schema))]
1197#[serde(rename_all = "camelCase")]
1198#[non_exhaustive]
1199pub struct OtherSessionUpdate {
1200    /// Custom or future session update type.
1201    ///
1202    /// Values beginning with `_` are reserved for implementation-specific
1203    /// extensions. Unknown values that do not begin with `_` are reserved for
1204    /// future ACP variants.
1205    #[serde(rename = "sessionUpdate")]
1206    pub session_update: String,
1207    /// Additional fields from the unknown update payload.
1208    #[serde(flatten)]
1209    pub fields: BTreeMap<String, serde_json::Value>,
1210}
1211
1212impl OtherSessionUpdate {
1213    /// Builds [`OtherSessionUpdate`] from an unknown discriminator and preserves the remaining extension fields.
1214    #[must_use]
1215    pub fn new(
1216        session_update: impl Into<String>,
1217        mut fields: BTreeMap<String, serde_json::Value>,
1218    ) -> Self {
1219        fields.remove("sessionUpdate");
1220        Self {
1221            session_update: session_update.into(),
1222            fields,
1223        }
1224    }
1225}
1226
1227impl<'de> Deserialize<'de> for OtherSessionUpdate {
1228    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1229    where
1230        D: serde::Deserializer<'de>,
1231    {
1232        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1233        let session_update = fields
1234            .remove("sessionUpdate")
1235            .ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
1236        let serde_json::Value::String(session_update) = session_update else {
1237            return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
1238        };
1239
1240        if is_known_session_update(&session_update) {
1241            return Err(serde::de::Error::custom(format!(
1242                "known session update `{session_update}` did not match its schema"
1243            )));
1244        }
1245
1246        Ok(Self {
1247            session_update,
1248            fields,
1249        })
1250    }
1251}
1252
1253fn is_known_session_update(session_update: &str) -> bool {
1254    if session_update == "notice" {
1255        return true;
1256    }
1257    if matches!(
1258        session_update,
1259        "compaction_update" | "compaction_summary_chunk"
1260    ) {
1261        return true;
1262    }
1263    #[cfg(feature = "unstable_plan_operations")]
1264    if session_update == "plan_removed" {
1265        return true;
1266    }
1267    #[cfg(feature = "unstable_subagents")]
1268    if matches!(
1269        session_update,
1270        "subagent_update" | "session_message" | "session_message_chunk"
1271    ) {
1272        return true;
1273    }
1274    matches!(
1275        session_update,
1276        "user_message_chunk"
1277            | "user_message"
1278            | "agent_message_chunk"
1279            | "agent_message"
1280            | "agent_thought_chunk"
1281            | "agent_thought"
1282            | "state_update"
1283            | "tool_call_content_chunk"
1284            | "tool_call_update"
1285            | "terminal_update"
1286            | "terminal_output_chunk"
1287            | "plan_update"
1288            | "available_commands_update"
1289            | "config_option_update"
1290            | "session_info_update"
1291            | "usage_update"
1292    )
1293}
1294
1295#[cfg(feature = "schemars")]
1296fn other_session_update_schema(schema: &mut Schema) {
1297    super::schema_util::reject_known_string_discriminators(
1298        schema,
1299        "sessionUpdate",
1300        &[
1301            "user_message_chunk",
1302            "user_message",
1303            "agent_message_chunk",
1304            "agent_message",
1305            "agent_thought_chunk",
1306            "agent_thought",
1307            "state_update",
1308            "tool_call_content_chunk",
1309            "tool_call_update",
1310            "terminal_update",
1311            "terminal_output_chunk",
1312            "plan_update",
1313            "available_commands_update",
1314            "config_option_update",
1315            "session_info_update",
1316            #[cfg(feature = "unstable_plan_operations")]
1317            "plan_removed",
1318            "usage_update",
1319            "notice",
1320            "compaction_update",
1321            "compaction_summary_chunk",
1322            #[cfg(feature = "unstable_subagents")]
1323            "subagent_update",
1324            #[cfg(feature = "unstable_subagents")]
1325            "session_message",
1326            #[cfg(feature = "unstable_subagents")]
1327            "session_message_chunk",
1328        ],
1329    );
1330}
1331
1332/// Session configuration options have been updated.
1333#[serde_as]
1334#[skip_serializing_none]
1335#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1337#[serde(rename_all = "camelCase")]
1338#[non_exhaustive]
1339pub struct ConfigOptionUpdate {
1340    /// The full set of configuration options and their current values.
1341    #[serde_as(deserialize_as = "VecSkipError<_>")]
1342    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
1343    pub config_options: Vec<SessionConfigOption>,
1344    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1345    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1346    /// Implementations MUST NOT make assumptions about values at these keys.
1347    ///
1348    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1349    #[serde_as(deserialize_as = "DefaultOnError")]
1350    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1351    #[serde(default)]
1352    #[serde(rename = "_meta")]
1353    pub meta: Option<Meta>,
1354}
1355
1356impl ConfigOptionUpdate {
1357    /// Builds [`ConfigOptionUpdate`] with the required fields set; optional fields start unset or empty.
1358    #[must_use]
1359    pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
1360        Self {
1361            config_options,
1362            meta: None,
1363        }
1364    }
1365
1366    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1367    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1368    /// these keys.
1369    ///
1370    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1371    #[must_use]
1372    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1373        self.meta = meta.into_option();
1374        self
1375    }
1376}
1377
1378/// Update to session metadata. All fields are optional to support partial updates.
1379///
1380/// Agents send this notification to update session information like title or custom metadata.
1381/// This allows clients to display dynamic session names and track session state changes.
1382///
1383/// Omitted fields leave the existing session info unchanged. `null` clears the
1384/// corresponding value. A concrete `_meta` object patches the stored metadata by
1385/// top-level key.
1386#[serde_as]
1387#[skip_serializing_none]
1388#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1389#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1390#[serde(rename_all = "camelCase")]
1391#[non_exhaustive]
1392pub struct SessionInfoUpdate {
1393    /// Human-readable title for the session. Set to null to clear.
1394    #[serde_as(deserialize_as = "DefaultOnError")]
1395    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1396    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1397    pub title: MaybeUndefined<String>,
1398    /// RFC 3339 timestamp of last activity. Set to null to clear.
1399    #[serde_as(deserialize_as = "DefaultOnError")]
1400    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
1401    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1402    pub updated_at: MaybeUndefined<String>,
1403    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1404    /// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
1405    /// clears it. An object patches the stored metadata by top-level key: keys it omits are
1406    /// unchanged, a key set to `null` is removed, and any other value replaces that key's
1407    /// stored value without merging nested objects. Beyond this patch rule, implementations
1408    /// MUST NOT make assumptions about values at these keys.
1409    ///
1410    /// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/v2/extensibility#metadata-patches)
1411    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1412    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1413    #[serde(
1414        rename = "_meta",
1415        default,
1416        skip_serializing_if = "MaybeUndefined::is_undefined"
1417    )]
1418    pub meta: MaybeUndefined<Meta>,
1419}
1420
1421impl SessionInfoUpdate {
1422    /// Builds [`SessionInfoUpdate`] with the required fields set; optional fields start unset or empty.
1423    #[must_use]
1424    pub fn new() -> Self {
1425        Self::default()
1426    }
1427
1428    /// Human-readable title for the session. Set to null to clear.
1429    #[must_use]
1430    pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1431        self.title = title.into_maybe_undefined();
1432        self
1433    }
1434
1435    /// RFC 3339 timestamp of last activity. Set to null to clear.
1436    #[must_use]
1437    pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
1438        self.updated_at = updated_at.into_maybe_undefined();
1439        self
1440    }
1441
1442    /// Patches, clears, or leaves unchanged session metadata.
1443    ///
1444    /// A concrete object patches stored metadata by top-level key; see
1445    /// [`apply_meta_update`](super::apply_meta_update).
1446    #[must_use]
1447    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1448        self.meta = meta.into_maybe_undefined();
1449        self
1450    }
1451}
1452
1453/// Context window and cost update for a session.
1454#[serde_as]
1455#[skip_serializing_none]
1456#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1458#[serde(rename_all = "camelCase")]
1459#[non_exhaustive]
1460pub struct UsageUpdate {
1461    /// Tokens currently in context.
1462    pub used: u64,
1463    /// Total context window size in tokens.
1464    pub size: u64,
1465    /// Cumulative session cost (optional).
1466    ///
1467    /// Omitted and `null` are equivalent and mean no cost is reported.
1468    #[serde_as(deserialize_as = "DefaultOnError")]
1469    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1470    #[serde(default)]
1471    pub cost: Option<Cost>,
1472    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1473    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1474    /// Implementations MUST NOT make assumptions about values at these keys.
1475    ///
1476    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1477    #[serde_as(deserialize_as = "DefaultOnError")]
1478    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1479    #[serde(default)]
1480    #[serde(rename = "_meta")]
1481    pub meta: Option<Meta>,
1482}
1483
1484impl UsageUpdate {
1485    /// Builds [`UsageUpdate`] with the required fields set; optional fields start unset or empty.
1486    #[must_use]
1487    pub fn new(used: u64, size: u64) -> Self {
1488        Self {
1489            used,
1490            size,
1491            cost: None,
1492            meta: None,
1493        }
1494    }
1495
1496    /// Cumulative session cost (optional).
1497    #[must_use]
1498    pub fn cost(mut self, cost: impl IntoOption<Cost>) -> Self {
1499        self.cost = cost.into_option();
1500        self
1501    }
1502
1503    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1504    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1505    /// these keys.
1506    ///
1507    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1508    #[must_use]
1509    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1510        self.meta = meta.into_option();
1511        self
1512    }
1513}
1514
1515/// A snapshot of the session's aggregate foreground activity.
1516///
1517/// Foreground work is the agent's active response or workflow in the session,
1518/// including model processing, tool execution, and waiting for user action needed
1519/// to continue that work. It can start with a prompt or be agent-initiated, for
1520/// example to pursue an ongoing goal or follow up on a completed background task.
1521/// Either way, the agent MUST report `running` when foreground work starts,
1522/// or `requires_action` if it starts blocked on user action, and `idle` when it
1523/// ends. The agent SHOULD include a stop reason on that idle update.
1524/// Agent-initiated work does not require a prompt request or a synthetic user
1525/// message.
1526///
1527/// `running` may precede user-message insertion, for example during a foreground
1528/// hook. It is not acknowledgment of insertion, and rejecting a prompt does not
1529/// itself end foreground work.
1530///
1531/// Background activity can continue and emit other `session/update` notifications
1532/// while `idle`. Those notifications, including a detached task's progress or
1533/// completion, do not by themselves change this state. A permission request only
1534/// warrants `requires_action` when foreground work is blocked on user action.
1535///
1536/// The agent SHOULD report `requires_action` when the foreground workflow as a
1537/// whole cannot progress without user action, including permissions or elicitations.
1538/// Once reported, it MUST report `running` when progress resumes, or `idle` when
1539/// work ends without resuming.
1540/// If other foreground work can progress, the state remains `running`. Resolving
1541/// one of several requests does not itself change the state. Agents SHOULD ignore
1542/// responses for work that has already ended rather than use them to resume it.
1543///
1544/// A new session defaults to idle until a state update reports otherwise.
1545/// Resume MUST send a current-state snapshot after any replay and before its
1546/// response. The snapshot reports foreground state after resuming.
1547/// The response alone does not imply idle.
1548/// Repeated snapshots are idempotent, not additional work, completions, or usage.
1549/// A repeated state may carry updated information without another state transition.
1550/// The agent MUST report `idle` when no foreground activity remains. Completing
1551/// an individual activity while other foreground work continues does not require
1552/// an idle boundary. Local commands use the same lifecycle even without model work.
1553///
1554/// See protocol docs: [Session States](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#session-states)
1555#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1557#[serde(tag = "state", rename_all = "snake_case")]
1558#[non_exhaustive]
1559pub enum StateUpdate {
1560    /// Foreground work is in progress.
1561    Running(RunningStateUpdate),
1562    /// No foreground work is active. Background activity may continue.
1563    Idle(IdleStateUpdate),
1564    /// Foreground work is blocked on user action.
1565    RequiresAction(RequiresActionStateUpdate),
1566    /// **UNSTABLE**
1567    ///
1568    /// This capability is not part of the spec yet, and may be removed or changed at any point.
1569    ///
1570    /// The Agent cannot currently determine foreground activity.
1571    /// This replaces previously confirmed activity without ending the work or session.
1572    #[cfg(feature = "unstable_subagents")]
1573    Unknown(UnknownStateUpdate),
1574    /// Custom or future session state.
1575    ///
1576    /// Values beginning with `_` are reserved for implementation-specific
1577    /// extensions. Unknown values that do not begin with `_` are reserved for
1578    /// future ACP variants.
1579    ///
1580    /// Unless the Client advertised support for this state, Agents MUST still
1581    /// report the standard states.
1582    /// Clients MUST NOT treat an unrecognized state as `idle` or present the
1583    /// previous state as confirmed, and show a generic busy state until a known
1584    /// state arrives.
1585    #[serde(untagged)]
1586    Other(OtherStateUpdate),
1587}
1588
1589/// Foreground work is in progress.
1590#[serde_as]
1591#[skip_serializing_none]
1592#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1593#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1594#[serde(rename_all = "camelCase")]
1595#[non_exhaustive]
1596pub struct RunningStateUpdate {
1597    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1598    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1599    /// Implementations MUST NOT make assumptions about values at these keys.
1600    ///
1601    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1602    #[serde_as(deserialize_as = "DefaultOnError")]
1603    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1604    #[serde(default)]
1605    #[serde(rename = "_meta")]
1606    pub meta: Option<Meta>,
1607}
1608
1609impl RunningStateUpdate {
1610    /// Builds [`RunningStateUpdate`] with the required fields set; optional fields start unset or empty.
1611    #[must_use]
1612    pub fn new() -> Self {
1613        Self::default()
1614    }
1615
1616    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1617    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1618    /// these keys.
1619    ///
1620    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1621    #[must_use]
1622    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1623        self.meta = meta.into_option();
1624        self
1625    }
1626}
1627
1628/// No foreground work is active. Background activity may continue.
1629///
1630/// Agents SHOULD include a `stopReason` when the idle transition ends foreground
1631/// work. An idle update that does not end foreground work need not include one.
1632/// Receivers treat an omitted, `null`, or malformed `stopReason` as the agent not
1633/// reporting one.
1634/// Repeated or resume snapshots may carry the last stop reason for context.
1635/// They MUST NOT be counted as new completions, cancellations, or additional usage.
1636#[serde_as]
1637#[skip_serializing_none]
1638#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1639#[cfg_attr(feature = "schemars", schemars(transform = idle_state_update_schema))]
1640#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1641#[serde(rename_all = "camelCase")]
1642#[non_exhaustive]
1643pub struct IdleStateUpdate {
1644    /// Indicates why foreground work stopped, with any fields of that stop reason.
1645    #[serde(flatten)]
1646    pub stop_reason: Option<StopReason>,
1647    /// **UNSTABLE**
1648    ///
1649    /// This capability is not part of the spec yet, and may be removed or changed at any point.
1650    ///
1651    /// Token usage for completed foreground work.
1652    ///
1653    /// Optional. Omitted and `null` are equivalent and mean the agent is not reporting token
1654    /// usage for this state update.
1655    #[cfg(feature = "unstable_end_turn_token_usage")]
1656    #[serde_as(deserialize_as = "DefaultOnError")]
1657    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1658    #[serde(default)]
1659    pub usage: Option<Box<Usage>>,
1660    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1661    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1662    /// Implementations MUST NOT make assumptions about values at these keys.
1663    ///
1664    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1665    #[serde_as(deserialize_as = "DefaultOnError")]
1666    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1667    #[serde(default)]
1668    #[serde(rename = "_meta")]
1669    pub meta: Option<Meta>,
1670}
1671
1672impl IdleStateUpdate {
1673    /// Builds [`IdleStateUpdate`] with the required fields set; optional fields start unset or empty.
1674    #[must_use]
1675    pub fn new() -> Self {
1676        Self::default()
1677    }
1678
1679    /// Indicates why foreground work stopped.
1680    #[must_use]
1681    pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
1682        self.stop_reason = stop_reason.into_option();
1683        self
1684    }
1685
1686    /// **UNSTABLE**
1687    ///
1688    /// This capability is not part of the spec yet, and may be removed or changed at any point.
1689    ///
1690    /// Token usage for completed foreground work.
1691    #[cfg(feature = "unstable_end_turn_token_usage")]
1692    #[must_use]
1693    pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
1694        self.usage = usage.into_option().map(Box::new);
1695        self
1696    }
1697
1698    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1699    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1700    /// these keys.
1701    ///
1702    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1703    #[must_use]
1704    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1705        self.meta = meta.into_option();
1706        self
1707    }
1708}
1709
1710/// Makes the schema check stop reasons and keeps their default-on-error hint.
1711///
1712/// Flattening `Option<StopReason>` adds an empty `{}` branch to the `anyOf`. It
1713/// matches any value, so the stop reason branches would check nothing; this
1714/// replaces it with a branch for an omitted or `null` `stopReason`. Flattening
1715/// also removes the `stopReason` property that carried
1716/// `x-deserialize-default-on-error`, which SDKs generated from the schema rely on
1717/// to tolerate a malformed value, so this adds it back as a shared property.
1718#[cfg(feature = "schemars")]
1719fn idle_state_update_schema(schema: &mut Schema) {
1720    if let Some(properties) = schema
1721        .get_mut("properties")
1722        .and_then(serde_json::Value::as_object_mut)
1723    {
1724        properties.insert(
1725            "stopReason".into(),
1726            serde_json::json!({
1727                "description": "Why foreground work stopped. The value selects one of this type's variants, which may add fields of their own.\n\nOptional. Omitted and `null` are equivalent and mean the agent is not reporting a stop reason; a malformed value is treated the same way.\n\nSee protocol docs: [Stop Reasons](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#stop-reasons)",
1728                "type": ["string", "null"],
1729                "x-deserialize-default-on-error": true
1730            }),
1731        );
1732    }
1733    let Some(variants) = schema
1734        .get_mut("anyOf")
1735        .and_then(serde_json::Value::as_array_mut)
1736    else {
1737        return;
1738    };
1739    for variant in variants {
1740        if variant.as_object().is_some_and(serde_json::Map::is_empty) {
1741            *variant = serde_json::json!({
1742                "title": "none",
1743                "description": "No stop reason: `stopReason` is omitted or `null`.",
1744                "type": "object",
1745                "properties": {
1746                    "stopReason": { "type": "null" }
1747                }
1748            });
1749        }
1750    }
1751}
1752
1753/// The foreground workflow as a whole is blocked on user action.
1754///
1755/// Agents SHOULD report this state when foreground work cannot progress without
1756/// user action. Pending permissions or elicitations alone do not imply this
1757/// state if other foreground work can progress, or if the requests belong only
1758/// to independent background activity. Once reported, agents MUST report
1759/// `running` when progress resumes, or `idle` if the work ends without resuming.
1760/// Responding to one request does not itself imply `running`.
1761#[serde_as]
1762#[skip_serializing_none]
1763#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1764#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1765#[serde(rename_all = "camelCase")]
1766#[non_exhaustive]
1767pub struct RequiresActionStateUpdate {
1768    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1769    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1770    /// Implementations MUST NOT make assumptions about values at these keys.
1771    ///
1772    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1773    #[serde_as(deserialize_as = "DefaultOnError")]
1774    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1775    #[serde(default)]
1776    #[serde(rename = "_meta")]
1777    pub meta: Option<Meta>,
1778}
1779
1780impl RequiresActionStateUpdate {
1781    /// Builds [`RequiresActionStateUpdate`] with the required fields set; optional fields start unset or empty.
1782    #[must_use]
1783    pub fn new() -> Self {
1784        Self::default()
1785    }
1786
1787    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1788    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1789    /// these keys.
1790    ///
1791    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1792    #[must_use]
1793    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1794        self.meta = meta.into_option();
1795        self
1796    }
1797}
1798
1799/// **UNSTABLE**
1800///
1801/// This capability is not part of the spec yet, and may be removed or changed at any point.
1802///
1803/// The Agent cannot currently determine foreground activity.
1804///
1805/// Report this when activity becomes unobservable, not merely because the session
1806/// has been quiet. The Client MUST stop presenting the previous state as confirmed
1807/// current activity, but may retain it as last known. A later state replaces this
1808/// snapshot normally.
1809///
1810/// This is not a task outcome or session closure. It does not cancel work, resolve
1811/// pending requests, or revoke capabilities; capabilities are updated separately.
1812#[cfg(feature = "unstable_subagents")]
1813#[serde_as]
1814#[skip_serializing_none]
1815#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1816#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1817#[serde(rename_all = "camelCase")]
1818#[non_exhaustive]
1819pub struct UnknownStateUpdate {
1820    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1821    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1822    /// Implementations MUST NOT make assumptions about values at these keys.
1823    ///
1824    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1825    #[serde_as(deserialize_as = "DefaultOnError")]
1826    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1827    #[serde(default, rename = "_meta")]
1828    pub meta: Option<Meta>,
1829}
1830
1831#[cfg(feature = "unstable_subagents")]
1832impl UnknownStateUpdate {
1833    /// Builds an unknown-activity state.
1834    #[must_use]
1835    pub fn new() -> Self {
1836        Self::default()
1837    }
1838
1839    /// Sets optional metadata for this state snapshot.
1840    #[must_use]
1841    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1842        self.meta = meta.into_option();
1843        self
1844    }
1845}
1846
1847/// Custom or future session state payload.
1848///
1849/// This preserves the unknown `state` discriminator and the rest of the state
1850/// object for clients that store, replay, proxy, or forward session history.
1851#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1852#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1853#[cfg_attr(feature = "schemars", schemars(inline))]
1854#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
1855#[serde(rename_all = "camelCase")]
1856#[non_exhaustive]
1857pub struct OtherStateUpdate {
1858    /// Custom or future session state.
1859    ///
1860    /// Values beginning with `_` are reserved for implementation-specific
1861    /// extensions. Unknown values that do not begin with `_` are reserved for
1862    /// future ACP variants.
1863    #[serde(rename = "state")]
1864    pub state: String,
1865    /// Additional fields from the unknown state payload.
1866    #[serde(flatten)]
1867    pub fields: BTreeMap<String, serde_json::Value>,
1868}
1869
1870impl OtherStateUpdate {
1871    /// Builds [`OtherStateUpdate`] from an unknown discriminator and preserves the remaining extension fields.
1872    #[must_use]
1873    pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1874        fields.remove("state");
1875        Self {
1876            state: state.into(),
1877            fields,
1878        }
1879    }
1880}
1881
1882impl<'de> Deserialize<'de> for OtherStateUpdate {
1883    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1884    where
1885        D: serde::Deserializer<'de>,
1886    {
1887        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1888        let state = fields
1889            .remove("state")
1890            .ok_or_else(|| serde::de::Error::missing_field("state"))?;
1891        let serde_json::Value::String(state) = state else {
1892            return Err(serde::de::Error::custom("`state` must be a string"));
1893        };
1894
1895        if is_known_state_update(&state) {
1896            return Err(serde::de::Error::custom(format!(
1897                "known state update `{state}` did not match its schema"
1898            )));
1899        }
1900
1901        Ok(Self { state, fields })
1902    }
1903}
1904
1905const KNOWN_STATE_UPDATE_STATES: &[&str] = &[
1906    "running",
1907    "idle",
1908    "requires_action",
1909    #[cfg(feature = "unstable_subagents")]
1910    "unknown",
1911];
1912
1913fn is_known_state_update(state: &str) -> bool {
1914    KNOWN_STATE_UPDATE_STATES.contains(&state)
1915}
1916
1917#[cfg(feature = "schemars")]
1918fn other_state_update_schema(schema: &mut Schema) {
1919    super::schema_util::reject_known_string_discriminators(
1920        schema,
1921        "state",
1922        KNOWN_STATE_UPDATE_STATES,
1923    );
1924}
1925
1926/// Cost information for a session.
1927#[serde_as]
1928#[skip_serializing_none]
1929#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1930#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1931#[serde(rename_all = "camelCase")]
1932#[non_exhaustive]
1933pub struct Cost {
1934    /// Total cumulative cost for session.
1935    pub amount: f64,
1936    /// ISO 4217 currency code (e.g., "USD", "EUR").
1937    #[cfg_attr(feature = "schemars", schemars(pattern(r"^[A-Z]{3}$")))]
1938    pub currency: String,
1939    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1940    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1941    /// Implementations MUST NOT make assumptions about values at these keys.
1942    ///
1943    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1944    #[serde_as(deserialize_as = "DefaultOnError")]
1945    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1946    #[serde(default)]
1947    #[serde(rename = "_meta")]
1948    pub meta: Option<Meta>,
1949}
1950
1951impl Cost {
1952    /// Builds [`Cost`] with the required fields set; optional fields start unset or empty.
1953    #[must_use]
1954    pub fn new(amount: f64, currency: impl Into<String>) -> Self {
1955        Self {
1956            amount,
1957            currency: currency.into(),
1958            meta: None,
1959        }
1960    }
1961
1962    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1963    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1964    /// these keys.
1965    ///
1966    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1967    #[must_use]
1968    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1969        self.meta = meta.into_option();
1970        self
1971    }
1972}
1973
1974/// A streamed item of message content.
1975#[serde_as]
1976#[skip_serializing_none]
1977#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1978#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1979#[serde(rename_all = "camelCase")]
1980#[non_exhaustive]
1981pub struct ContentChunk {
1982    /// A unique identifier for the message this chunk belongs to.
1983    ///
1984    /// All chunks belonging to the same message share the same `messageId`.
1985    /// Chunks for different messages can interleave.
1986    pub message_id: MessageId,
1987    /// A single item of content
1988    pub content: ContentBlock,
1989    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1990    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
1991    /// Implementations MUST NOT make assumptions about values at these keys.
1992    /// This metadata is scoped to the chunk and does not patch stored metadata.
1993    ///
1994    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1995    #[serde_as(deserialize_as = "DefaultOnError")]
1996    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1997    #[serde(default)]
1998    #[serde(rename = "_meta")]
1999    pub meta: Option<Meta>,
2000}
2001
2002impl ContentChunk {
2003    /// Builds [`ContentChunk`] with the required fields set; optional fields start unset or empty.
2004    #[must_use]
2005    pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
2006        Self {
2007            content,
2008            message_id: message_id.into(),
2009            meta: None,
2010        }
2011    }
2012
2013    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2014    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2015    /// these keys. This field is chunk-scoped.
2016    ///
2017    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2018    #[must_use]
2019    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2020        self.meta = meta.into_option();
2021        self
2022    }
2023}
2024
2025/// A user message upsert.
2026///
2027/// Only `messageId` is required. `content` has patch semantics:
2028/// an omitted field leaves existing message content unchanged, `null` clears the
2029/// value, and a concrete array replaces the previous value. For a new
2030/// `messageId`, omitted fields use client defaults. `content` is replaced as a
2031/// whole array; send `[]` or `null` to clear it.
2032///
2033/// Message updates and chunks are applied in the order they are received. When
2034/// a `user_message` update includes `content`, that array replaces any content
2035/// previously accumulated for the message, including content from earlier
2036/// chunks. Later chunks with the same `messageId` append to the current
2037/// content.
2038#[serde_as]
2039#[skip_serializing_none]
2040#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2041#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2042#[serde(rename_all = "camelCase")]
2043#[non_exhaustive]
2044pub struct UserMessage {
2045    /// A unique identifier for the message.
2046    pub message_id: MessageId,
2047    /// Complete replacement content for this message.
2048    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2049    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2050    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2051    pub content: MaybeUndefined<Vec<ContentBlock>>,
2052    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2053    /// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
2054    /// clears it. An object patches the stored metadata by top-level key: keys it omits are
2055    /// unchanged, a key set to `null` is removed, and any other value replaces that key's
2056    /// stored value without merging nested objects. Beyond this patch rule, implementations
2057    /// MUST NOT make assumptions about values at these keys.
2058    ///
2059    /// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/v2/extensibility#metadata-patches)
2060    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2061    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2062    #[serde(
2063        rename = "_meta",
2064        default,
2065        skip_serializing_if = "MaybeUndefined::is_undefined"
2066    )]
2067    pub meta: MaybeUndefined<Meta>,
2068}
2069
2070impl UserMessage {
2071    /// Builds [`UserMessage`] with the required fields set; optional fields start unset or empty.
2072    #[must_use]
2073    pub fn new(message_id: impl Into<MessageId>) -> Self {
2074        Self {
2075            message_id: message_id.into(),
2076            content: MaybeUndefined::Undefined,
2077            meta: MaybeUndefined::Undefined,
2078        }
2079    }
2080
2081    /// Complete replacement content for this message.
2082    #[must_use]
2083    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2084        self.content = content.into_maybe_undefined();
2085        self
2086    }
2087
2088    /// Patches, clears, or leaves unchanged message metadata.
2089    ///
2090    /// A concrete object patches stored metadata by top-level key; see
2091    /// [`apply_meta_update`](super::apply_meta_update).
2092    #[must_use]
2093    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2094        self.meta = meta.into_maybe_undefined();
2095        self
2096    }
2097}
2098
2099/// An agent message upsert.
2100///
2101/// Only `messageId` is required. `content` has patch semantics:
2102/// an omitted field leaves existing message content unchanged, `null` clears the
2103/// value, and a concrete array replaces the previous value. For a new
2104/// `messageId`, omitted fields use client defaults. `content` is replaced as a
2105/// whole array; send `[]` or `null` to clear it.
2106///
2107/// Message updates and chunks are applied in the order they are received. When
2108/// an `agent_message` update includes `content`, that array replaces any
2109/// content previously accumulated for the message, including content from
2110/// earlier chunks. Later chunks with the same `messageId` append to the current
2111/// content.
2112#[serde_as]
2113#[skip_serializing_none]
2114#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2116#[serde(rename_all = "camelCase")]
2117#[non_exhaustive]
2118pub struct AgentMessage {
2119    /// A unique identifier for the message.
2120    pub message_id: MessageId,
2121    /// Complete replacement content for this message.
2122    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2123    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2124    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2125    pub content: MaybeUndefined<Vec<ContentBlock>>,
2126    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2127    /// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
2128    /// clears it. An object patches the stored metadata by top-level key: keys it omits are
2129    /// unchanged, a key set to `null` is removed, and any other value replaces that key's
2130    /// stored value without merging nested objects. Beyond this patch rule, implementations
2131    /// MUST NOT make assumptions about values at these keys.
2132    ///
2133    /// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/v2/extensibility#metadata-patches)
2134    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2135    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2136    #[serde(
2137        rename = "_meta",
2138        default,
2139        skip_serializing_if = "MaybeUndefined::is_undefined"
2140    )]
2141    pub meta: MaybeUndefined<Meta>,
2142}
2143
2144impl AgentMessage {
2145    /// Builds [`AgentMessage`] with the required fields set; optional fields start unset or empty.
2146    #[must_use]
2147    pub fn new(message_id: impl Into<MessageId>) -> Self {
2148        Self {
2149            message_id: message_id.into(),
2150            content: MaybeUndefined::Undefined,
2151            meta: MaybeUndefined::Undefined,
2152        }
2153    }
2154
2155    /// Complete replacement content for this message.
2156    #[must_use]
2157    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2158        self.content = content.into_maybe_undefined();
2159        self
2160    }
2161
2162    /// Patches, clears, or leaves unchanged message metadata.
2163    ///
2164    /// A concrete object patches stored metadata by top-level key; see
2165    /// [`apply_meta_update`](super::apply_meta_update).
2166    #[must_use]
2167    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2168        self.meta = meta.into_maybe_undefined();
2169        self
2170    }
2171}
2172
2173/// An agent thought or reasoning message upsert.
2174///
2175/// Only `messageId` is required. `content` has patch semantics:
2176/// an omitted field leaves existing thought content unchanged, `null` clears the
2177/// value, and a concrete array replaces the previous value. For a new
2178/// `messageId`, omitted fields use client defaults. `content` is replaced as a
2179/// whole array; send `[]` or `null` to clear it.
2180///
2181/// Message updates and chunks are applied in the order they are received. When
2182/// an `agent_thought` update includes `content`, that array replaces any
2183/// content previously accumulated for the thought, including content from
2184/// earlier chunks. Later chunks with the same `messageId` append to the current
2185/// content.
2186#[serde_as]
2187#[skip_serializing_none]
2188#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2190#[serde(rename_all = "camelCase")]
2191#[non_exhaustive]
2192pub struct AgentThought {
2193    /// A unique identifier for the thought message.
2194    pub message_id: MessageId,
2195    /// Complete replacement content for this thought message.
2196    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2197    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2198    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2199    pub content: MaybeUndefined<Vec<ContentBlock>>,
2200    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2201    /// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
2202    /// clears it. An object patches the stored metadata by top-level key: keys it omits are
2203    /// unchanged, a key set to `null` is removed, and any other value replaces that key's
2204    /// stored value without merging nested objects. Beyond this patch rule, implementations
2205    /// MUST NOT make assumptions about values at these keys.
2206    ///
2207    /// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/v2/extensibility#metadata-patches)
2208    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2209    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2210    #[serde(
2211        rename = "_meta",
2212        default,
2213        skip_serializing_if = "MaybeUndefined::is_undefined"
2214    )]
2215    pub meta: MaybeUndefined<Meta>,
2216}
2217
2218impl AgentThought {
2219    /// Builds [`AgentThought`] with the required fields set; optional fields start unset or empty.
2220    #[must_use]
2221    pub fn new(message_id: impl Into<MessageId>) -> Self {
2222        Self {
2223            message_id: message_id.into(),
2224            content: MaybeUndefined::Undefined,
2225            meta: MaybeUndefined::Undefined,
2226        }
2227    }
2228
2229    /// Complete replacement content for this thought message.
2230    #[must_use]
2231    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2232        self.content = content.into_maybe_undefined();
2233        self
2234    }
2235
2236    /// Patches, clears, or leaves unchanged thought metadata.
2237    ///
2238    /// A concrete object patches stored metadata by top-level key; see
2239    /// [`apply_meta_update`](super::apply_meta_update).
2240    #[must_use]
2241    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2242        self.meta = meta.into_maybe_undefined();
2243        self
2244    }
2245}
2246
2247/// Identifier for a message, unique among messages of the same type within a session.
2248///
2249/// Each message type, such as user messages, agent messages, and agent thoughts,
2250/// has its own ID space: messages of different types may share an ID and remain
2251/// distinct messages.
2252#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
2254#[serde(transparent)]
2255#[from(forward)]
2256#[non_exhaustive]
2257pub struct MessageId(pub Arc<str>);
2258
2259impl MessageId {
2260    /// Wraps a protocol string as a typed [`MessageId`].
2261    #[must_use]
2262    pub fn new(id: impl Into<Self>) -> Self {
2263        id.into()
2264    }
2265}
2266
2267/// Available commands are ready or have changed
2268#[serde_as]
2269#[skip_serializing_none]
2270#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2272#[serde(rename_all = "camelCase")]
2273#[non_exhaustive]
2274pub struct AvailableCommandsUpdate {
2275    /// Commands the agent can execute.
2276    #[serde_as(deserialize_as = "VecSkipError<_>")]
2277    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
2278    pub available_commands: Vec<AvailableCommand>,
2279    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2280    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
2281    /// Implementations MUST NOT make assumptions about values at these keys.
2282    ///
2283    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2284    #[serde_as(deserialize_as = "DefaultOnError")]
2285    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2286    #[serde(default)]
2287    #[serde(rename = "_meta")]
2288    pub meta: Option<Meta>,
2289}
2290
2291impl AvailableCommandsUpdate {
2292    /// Builds [`AvailableCommandsUpdate`] with the required fields set; optional fields start unset or empty.
2293    #[must_use]
2294    pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
2295        Self {
2296            available_commands,
2297            meta: None,
2298        }
2299    }
2300
2301    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2302    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2303    /// these keys.
2304    ///
2305    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2306    #[must_use]
2307    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2308        self.meta = meta.into_option();
2309        self
2310    }
2311}
2312
2313/// A command the agent advertises to the client.
2314///
2315/// The `type` field acts as the discriminator in the serialized JSON form.
2316#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2318#[serde(tag = "type", rename_all = "snake_case")]
2319#[non_exhaustive]
2320pub enum AvailableCommand {
2321    /// A slash command the user can invoke by including it in a prompt.
2322    Command(Command),
2323    /// Custom or future command kind.
2324    ///
2325    /// Values beginning with `_` are reserved for implementation-specific
2326    /// extensions. Unknown values that do not begin with `_` are reserved for
2327    /// future ACP variants.
2328    ///
2329    /// Clients that do not understand this command type should preserve the raw
2330    /// payload when storing, replaying, proxying, or forwarding command
2331    /// metadata, and otherwise ignore the command or display it as a plain
2332    /// command using its `name` and `description`.
2333    #[serde(untagged)]
2334    Other(OtherAvailableCommand),
2335}
2336
2337impl AvailableCommand {
2338    /// Command name (e.g., `create_plan`, `research_codebase`).
2339    #[must_use]
2340    pub fn name(&self) -> &str {
2341        match self {
2342            Self::Command(c) => &c.name,
2343            Self::Other(c) => &c.name,
2344        }
2345    }
2346
2347    /// Human-readable description of what the command does.
2348    #[must_use]
2349    pub fn description(&self) -> &str {
2350        match self {
2351            Self::Command(c) => &c.description,
2352            Self::Other(c) => &c.description,
2353        }
2354    }
2355
2356    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2357    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2358    /// these keys.
2359    ///
2360    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2361    #[must_use]
2362    pub fn meta(&self) -> Option<&Meta> {
2363        match self {
2364            Self::Command(c) => c.meta.as_ref(),
2365            Self::Other(c) => c.fields.get("_meta").and_then(serde_json::Value::as_object),
2366        }
2367    }
2368}
2369
2370/// Custom or future command payload.
2371#[serde_as]
2372#[skip_serializing_none]
2373#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2374#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2375#[cfg_attr(feature = "schemars", schemars(inline))]
2376#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_schema))]
2377#[serde(rename_all = "camelCase")]
2378#[non_exhaustive]
2379pub struct OtherAvailableCommand {
2380    /// Custom or future command type.
2381    ///
2382    /// Values beginning with `_` are reserved for implementation-specific
2383    /// extensions. Unknown values that do not begin with `_` are reserved for
2384    /// future ACP variants.
2385    #[serde(rename = "type")]
2386    pub type_: String,
2387    /// Command name.
2388    pub name: String,
2389    /// Human-readable description of what the command does.
2390    pub description: String,
2391    /// The remaining fields of the unknown command payload, preserved verbatim, including
2392    /// `_meta`.
2393    #[serde(flatten)]
2394    pub fields: BTreeMap<String, serde_json::Value>,
2395}
2396
2397impl OtherAvailableCommand {
2398    /// Builds [`OtherAvailableCommand`] from an unknown discriminator and preserves the remaining extension fields.
2399    #[must_use]
2400    pub fn new(
2401        type_: impl Into<String>,
2402        name: impl Into<String>,
2403        description: impl Into<String>,
2404        mut fields: BTreeMap<String, serde_json::Value>,
2405    ) -> Self {
2406        fields.remove("type");
2407        fields.remove("name");
2408        fields.remove("description");
2409        Self {
2410            type_: type_.into(),
2411            name: name.into(),
2412            description: description.into(),
2413            fields,
2414        }
2415    }
2416}
2417
2418impl<'de> Deserialize<'de> for OtherAvailableCommand {
2419    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2420    where
2421        D: serde::Deserializer<'de>,
2422    {
2423        #[derive(Deserialize)]
2424        struct RawOtherAvailableCommand {
2425            #[serde(rename = "type")]
2426            type_: String,
2427            name: String,
2428            description: String,
2429            #[serde(flatten)]
2430            fields: BTreeMap<String, serde_json::Value>,
2431        }
2432
2433        let raw = RawOtherAvailableCommand::deserialize(deserializer)?;
2434        if is_known_available_command_type(&raw.type_) {
2435            return Err(serde::de::Error::custom(format!(
2436                "known available command type `{}` did not match its schema",
2437                raw.type_
2438            )));
2439        }
2440
2441        Ok(Self {
2442            type_: raw.type_,
2443            name: raw.name,
2444            description: raw.description,
2445            fields: raw.fields,
2446        })
2447    }
2448}
2449
2450const KNOWN_AVAILABLE_COMMAND_TYPES: &[&str] = &["command"];
2451
2452fn is_known_available_command_type(type_: &str) -> bool {
2453    KNOWN_AVAILABLE_COMMAND_TYPES.contains(&type_)
2454}
2455
2456#[cfg(feature = "schemars")]
2457fn other_available_command_schema(schema: &mut Schema) {
2458    super::schema_util::reject_known_string_discriminators(
2459        schema,
2460        "type",
2461        KNOWN_AVAILABLE_COMMAND_TYPES,
2462    );
2463}
2464
2465/// A slash command the user can invoke by including it in a prompt.
2466///
2467/// The `type` discriminator value is `command`.
2468#[serde_as]
2469#[skip_serializing_none]
2470#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2472#[serde(rename_all = "camelCase")]
2473#[non_exhaustive]
2474pub struct Command {
2475    /// Command name (e.g., `create_plan`, `research_codebase`).
2476    pub name: String,
2477    /// Human-readable description of what the command does.
2478    pub description: String,
2479    /// Input for the command if required
2480    ///
2481    /// Omitted and `null` are equivalent and mean the command takes no input.
2482    #[serde_as(deserialize_as = "DefaultOnError")]
2483    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2484    #[serde(default)]
2485    pub input: Option<AvailableCommandInput>,
2486    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2487    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
2488    /// Implementations MUST NOT make assumptions about values at these keys.
2489    ///
2490    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2491    #[serde_as(deserialize_as = "DefaultOnError")]
2492    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2493    #[serde(default)]
2494    #[serde(rename = "_meta")]
2495    pub meta: Option<Meta>,
2496}
2497
2498impl Command {
2499    /// Builds [`Command`] with the required fields set; optional fields start unset or empty.
2500    #[must_use]
2501    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
2502        Self {
2503            name: name.into(),
2504            description: description.into(),
2505            input: None,
2506            meta: None,
2507        }
2508    }
2509
2510    /// Input for the command if required
2511    #[must_use]
2512    pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
2513        self.input = input.into_option();
2514        self
2515    }
2516
2517    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2518    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2519    /// these keys.
2520    ///
2521    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2522    #[must_use]
2523    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2524        self.meta = meta.into_option();
2525        self
2526    }
2527}
2528
2529/// The input specification for a command.
2530#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2531#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2532#[serde(tag = "type", rename_all = "snake_case")]
2533#[non_exhaustive]
2534pub enum AvailableCommandInput {
2535    /// All text that was typed after the command name is provided as input.
2536    #[serde(rename = "text")]
2537    Text(TextCommandInput),
2538    /// Custom or future command input specification.
2539    ///
2540    /// Values beginning with `_` are reserved for implementation-specific
2541    /// extensions. Unknown values that do not begin with `_` are reserved for
2542    /// future ACP variants.
2543    ///
2544    /// Clients that do not understand this input type should preserve the raw
2545    /// payload when storing, replaying, proxying, or forwarding command
2546    /// metadata, and otherwise ignore the input specification or display the
2547    /// command without structured input.
2548    #[serde(untagged)]
2549    Other(OtherAvailableCommandInput),
2550}
2551
2552/// Custom or future command input specification.
2553#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2554#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2555#[cfg_attr(feature = "schemars", schemars(inline))]
2556#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
2557#[serde(rename_all = "camelCase")]
2558#[non_exhaustive]
2559pub struct OtherAvailableCommandInput {
2560    /// Custom or future command input type.
2561    ///
2562    /// Values beginning with `_` are reserved for implementation-specific
2563    /// extensions. Unknown values that do not begin with `_` are reserved for
2564    /// future ACP variants.
2565    #[serde(rename = "type")]
2566    pub type_: String,
2567    /// Additional fields from the unknown command input payload.
2568    #[serde(flatten)]
2569    pub fields: BTreeMap<String, serde_json::Value>,
2570}
2571
2572impl OtherAvailableCommandInput {
2573    /// Builds [`OtherAvailableCommandInput`] from an unknown discriminator and preserves the remaining extension fields.
2574    #[must_use]
2575    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2576        fields.remove("type");
2577        Self {
2578            type_: type_.into(),
2579            fields,
2580        }
2581    }
2582}
2583
2584impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
2585    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2586    where
2587        D: serde::Deserializer<'de>,
2588    {
2589        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2590        let type_ = fields
2591            .remove("type")
2592            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2593        let serde_json::Value::String(type_) = type_ else {
2594            return Err(serde::de::Error::custom("`type` must be a string"));
2595        };
2596
2597        if is_known_available_command_input_type(&type_) {
2598            return Err(serde::de::Error::custom(format!(
2599                "known available command input type `{type_}` did not match its schema"
2600            )));
2601        }
2602
2603        Ok(Self { type_, fields })
2604    }
2605}
2606
2607const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
2608
2609fn is_known_available_command_input_type(type_: &str) -> bool {
2610    KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
2611}
2612
2613#[cfg(feature = "schemars")]
2614fn other_available_command_input_schema(schema: &mut Schema) {
2615    super::schema_util::reject_known_string_discriminators(
2616        schema,
2617        "type",
2618        KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
2619    );
2620}
2621
2622/// All text that was typed after the command name is provided as input.
2623#[serde_as]
2624#[skip_serializing_none]
2625#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2626#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2627#[serde(rename_all = "camelCase")]
2628#[non_exhaustive]
2629pub struct TextCommandInput {
2630    /// A hint to display when the input hasn't been provided yet
2631    pub hint: String,
2632    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2633    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
2634    /// Implementations MUST NOT make assumptions about values at these keys.
2635    ///
2636    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2637    #[serde_as(deserialize_as = "DefaultOnError")]
2638    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2639    #[serde(default)]
2640    #[serde(rename = "_meta")]
2641    pub meta: Option<Meta>,
2642}
2643
2644impl TextCommandInput {
2645    /// Builds [`TextCommandInput`] with the required fields set; optional fields start unset or empty.
2646    #[must_use]
2647    pub fn new(hint: impl Into<String>) -> Self {
2648        Self {
2649            hint: hint.into(),
2650            meta: None,
2651        }
2652    }
2653
2654    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2655    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2656    /// these keys.
2657    ///
2658    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2659    #[must_use]
2660    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2661        self.meta = meta.into_option();
2662        self
2663    }
2664}
2665
2666// Permission
2667
2668/// Request for user permission to proceed with an operation.
2669///
2670/// Sent when the agent needs authorization before performing a sensitive operation.
2671///
2672/// See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/tool-calls#requesting-permission)
2673#[serde_as]
2674#[skip_serializing_none]
2675#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2677#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
2678#[serde(rename_all = "camelCase")]
2679#[non_exhaustive]
2680pub struct RequestPermissionRequest {
2681    /// The session ID for this request.
2682    pub session_id: SessionId,
2683    /// Human-readable title for the permission prompt.
2684    ///
2685    /// This title is specific to the permission prompt and does not update any
2686    /// subject's displayed title.
2687    pub title: String,
2688    /// Optional human-readable explanation of why permission is needed.
2689    ///
2690    /// This text is specific to the permission prompt and does not update any
2691    /// subject's displayed content. Omitted and `null` are equivalent and mean no separate
2692    /// permission description was provided.
2693    #[serde_as(deserialize_as = "DefaultOnError")]
2694    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2695    #[serde(default)]
2696    pub description: Option<String>,
2697    /// Optional structured context about the operation requiring permission.
2698    ///
2699    /// Omitted and `null` are equivalent and mean no structured subject was provided.
2700    #[serde(default)]
2701    pub subject: Option<RequestPermissionSubject>,
2702    /// Available permission options for the user to choose from.
2703    /// Must contain at least one option.
2704    #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
2705    pub options: Vec<PermissionOption>,
2706    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2707    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
2708    /// Implementations MUST NOT make assumptions about values at these keys.
2709    ///
2710    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2711    #[serde_as(deserialize_as = "DefaultOnError")]
2712    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2713    #[serde(default)]
2714    #[serde(rename = "_meta")]
2715    pub meta: Option<Meta>,
2716}
2717
2718impl RequestPermissionRequest {
2719    /// Builds [`RequestPermissionRequest`] with the required request fields set; optional fields start unset or empty.
2720    #[must_use]
2721    pub fn new(
2722        session_id: impl Into<SessionId>,
2723        title: impl Into<String>,
2724        options: Vec<PermissionOption>,
2725    ) -> Self {
2726        Self {
2727            session_id: session_id.into(),
2728            title: title.into(),
2729            description: None,
2730            subject: None,
2731            options,
2732            meta: None,
2733        }
2734    }
2735
2736    /// Sets or clears the optional `description` field.
2737    #[must_use]
2738    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
2739        self.description = description.into_option();
2740        self
2741    }
2742
2743    /// Sets or clears the optional `subject` field.
2744    #[must_use]
2745    pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
2746        self.subject = subject.into_option();
2747        self
2748    }
2749
2750    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2751    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2752    /// these keys.
2753    ///
2754    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2755    #[must_use]
2756    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2757        self.meta = meta.into_option();
2758        self
2759    }
2760}
2761
2762/// The operation requiring permission.
2763#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2764#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2765#[serde(tag = "type", rename_all = "snake_case")]
2766#[non_exhaustive]
2767pub enum RequestPermissionSubject {
2768    /// Permission is requested before executing a tool call.
2769    ToolCall(Box<ToolCallPermissionSubject>),
2770    /// Permission is requested before running a command.
2771    Command(CommandPermissionSubject),
2772    /// Custom or future permission subject.
2773    ///
2774    /// Values beginning with `_` are reserved for implementation-specific
2775    /// extensions. Unknown values that do not begin with `_` are reserved for
2776    /// future ACP variants.
2777    ///
2778    /// Clients that do not understand this subject type should preserve the raw
2779    /// payload when storing, replaying, proxying, or forwarding permission
2780    /// requests, and otherwise display a generic permission prompt or decline it
2781    /// according to policy.
2782    #[serde(untagged)]
2783    Other(OtherRequestPermissionSubject),
2784}
2785
2786impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
2787    fn from(subject: ToolCallPermissionSubject) -> Self {
2788        Self::ToolCall(Box::new(subject))
2789    }
2790}
2791
2792impl From<ToolCallUpdate> for RequestPermissionSubject {
2793    fn from(tool_call: ToolCallUpdate) -> Self {
2794        ToolCallPermissionSubject::new(tool_call).into()
2795    }
2796}
2797
2798impl From<CommandPermissionSubject> for RequestPermissionSubject {
2799    fn from(subject: CommandPermissionSubject) -> Self {
2800        Self::Command(subject)
2801    }
2802}
2803
2804/// Permission request details for a tool call.
2805#[serde_as]
2806#[skip_serializing_none]
2807#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2808#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2809#[serde(rename_all = "camelCase")]
2810#[non_exhaustive]
2811pub struct ToolCallPermissionSubject {
2812    /// Details about the tool call requiring permission.
2813    ///
2814    /// Clients apply this to their stored tool call exactly like a `tool_call_update`
2815    /// notification, in the order it was received, before showing the prompt. A first-seen
2816    /// `toolCallId` creates the tool call.
2817    pub tool_call: ToolCallUpdate,
2818    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2819    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
2820    /// Implementations MUST NOT make assumptions about values at these keys.
2821    ///
2822    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2823    #[serde_as(deserialize_as = "DefaultOnError")]
2824    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2825    #[serde(default)]
2826    #[serde(rename = "_meta")]
2827    pub meta: Option<Meta>,
2828}
2829
2830impl ToolCallPermissionSubject {
2831    /// Builds [`ToolCallPermissionSubject`] with the required fields set.
2832    #[must_use]
2833    pub fn new(tool_call: ToolCallUpdate) -> Self {
2834        Self {
2835            tool_call,
2836            meta: None,
2837        }
2838    }
2839
2840    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2841    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2842    /// these keys.
2843    ///
2844    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2845    #[must_use]
2846    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2847        self.meta = meta.into_option();
2848        self
2849    }
2850}
2851
2852/// Permission request details for a command.
2853#[serde_as]
2854#[skip_serializing_none]
2855#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2856#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2857#[serde(rename_all = "camelCase")]
2858#[non_exhaustive]
2859pub struct CommandPermissionSubject {
2860    /// The command that would be run if permission is granted.
2861    pub command: String,
2862    /// The absolute working directory for the command.
2863    pub cwd: AbsolutePath,
2864    /// The associated tool call, when known. Omitted and `null` are equivalent.
2865    #[serde_as(deserialize_as = "DefaultOnError")]
2866    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2867    #[serde(default)]
2868    pub tool_call_id: Option<ToolCallId>,
2869    /// The associated terminal, when already known. Omitted and `null` are equivalent.
2870    #[serde_as(deserialize_as = "DefaultOnError")]
2871    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2872    #[serde(default)]
2873    pub terminal_id: Option<TerminalId>,
2874    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2875    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
2876    /// Implementations MUST NOT make assumptions about values at these keys.
2877    ///
2878    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
2879    #[serde_as(deserialize_as = "DefaultOnError")]
2880    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2881    #[serde(default)]
2882    #[serde(rename = "_meta")]
2883    pub meta: Option<Meta>,
2884}
2885
2886impl CommandPermissionSubject {
2887    /// Builds command permission details with the required command and working directory.
2888    #[must_use]
2889    pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
2890        Self {
2891            command: command.into(),
2892            cwd: cwd.into(),
2893            tool_call_id: None,
2894            terminal_id: None,
2895            meta: None,
2896        }
2897    }
2898
2899    /// Sets or clears the associated tool-call ID.
2900    #[must_use]
2901    pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
2902        self.tool_call_id = tool_call_id.into_option();
2903        self
2904    }
2905
2906    /// Sets or clears the associated terminal ID.
2907    #[must_use]
2908    pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
2909        self.terminal_id = terminal_id.into_option();
2910        self
2911    }
2912
2913    /// Sets or clears subject-scoped metadata.
2914    #[must_use]
2915    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2916        self.meta = meta.into_option();
2917        self
2918    }
2919}
2920
2921/// Custom or future permission subject payload.
2922#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2923#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2924#[cfg_attr(feature = "schemars", schemars(inline))]
2925#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
2926#[serde(rename_all = "camelCase")]
2927#[non_exhaustive]
2928pub struct OtherRequestPermissionSubject {
2929    /// Custom or future permission subject type.
2930    ///
2931    /// Values beginning with `_` are reserved for implementation-specific
2932    /// extensions. Unknown values that do not begin with `_` are reserved for
2933    /// future ACP variants.
2934    #[serde(rename = "type")]
2935    pub type_: String,
2936    /// Additional fields from the unknown permission subject payload.
2937    #[serde(flatten)]
2938    pub fields: BTreeMap<String, serde_json::Value>,
2939}
2940
2941impl OtherRequestPermissionSubject {
2942    /// Builds [`OtherRequestPermissionSubject`] from an unknown discriminator and preserves the remaining extension fields.
2943    #[must_use]
2944    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2945        fields.remove("type");
2946        Self {
2947            type_: type_.into(),
2948            fields,
2949        }
2950    }
2951}
2952
2953impl<'de> Deserialize<'de> for OtherRequestPermissionSubject {
2954    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2955    where
2956        D: serde::Deserializer<'de>,
2957    {
2958        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2959        let type_ = fields
2960            .remove("type")
2961            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2962        let serde_json::Value::String(type_) = type_ else {
2963            return Err(serde::de::Error::custom("`type` must be a string"));
2964        };
2965
2966        if is_known_request_permission_subject_type(&type_) {
2967            return Err(serde::de::Error::custom(format!(
2968                "known request permission subject `{type_}` did not match its schema"
2969            )));
2970        }
2971
2972        Ok(Self { type_, fields })
2973    }
2974}
2975
2976fn is_known_request_permission_subject_type(type_: &str) -> bool {
2977    matches!(type_, "tool_call" | "command")
2978}
2979
2980#[cfg(feature = "schemars")]
2981fn other_request_permission_subject_schema(schema: &mut Schema) {
2982    super::schema_util::reject_known_string_discriminators(
2983        schema,
2984        "type",
2985        &["tool_call", "command"],
2986    );
2987}
2988
2989/// An option presented to the user when requesting permission.
2990#[serde_as]
2991#[skip_serializing_none]
2992#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2993#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2994#[serde(rename_all = "camelCase")]
2995#[non_exhaustive]
2996pub struct PermissionOption {
2997    /// Unique identifier for this permission option.
2998    pub option_id: PermissionOptionId,
2999    /// Human-readable label to display to the user.
3000    pub name: String,
3001    /// Hint about the nature of this permission option.
3002    pub kind: PermissionOptionKind,
3003    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3004    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
3005    /// Implementations MUST NOT make assumptions about values at these keys.
3006    ///
3007    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3008    #[serde_as(deserialize_as = "DefaultOnError")]
3009    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3010    #[serde(default)]
3011    #[serde(rename = "_meta")]
3012    pub meta: Option<Meta>,
3013}
3014
3015impl PermissionOption {
3016    /// Builds [`PermissionOption`] with the required fields set; optional fields start unset or empty.
3017    #[must_use]
3018    pub fn new(
3019        option_id: impl Into<PermissionOptionId>,
3020        name: impl Into<String>,
3021        kind: PermissionOptionKind,
3022    ) -> Self {
3023        Self {
3024            option_id: option_id.into(),
3025            name: name.into(),
3026            kind,
3027            meta: None,
3028        }
3029    }
3030
3031    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3032    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
3033    /// these keys.
3034    ///
3035    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3036    #[must_use]
3037    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3038        self.meta = meta.into_option();
3039        self
3040    }
3041}
3042
3043/// Unique identifier for a permission option.
3044#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3045#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
3046#[serde(transparent)]
3047#[from(forward)]
3048#[non_exhaustive]
3049pub struct PermissionOptionId(pub Arc<str>);
3050
3051impl PermissionOptionId {
3052    /// Wraps a protocol string as a typed [`PermissionOptionId`].
3053    #[must_use]
3054    pub fn new(id: impl Into<Self>) -> Self {
3055        id.into()
3056    }
3057}
3058
3059/// The type of permission option being presented to the user.
3060///
3061/// Helps clients choose appropriate icons and UI treatment.
3062#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3063#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3064#[serde(rename_all = "snake_case")]
3065#[non_exhaustive]
3066pub enum PermissionOptionKind {
3067    /// Allow this operation only this time.
3068    AllowOnce,
3069    /// Allow this operation and remember the choice.
3070    AllowAlways,
3071    /// Reject this operation only this time.
3072    RejectOnce,
3073    /// Reject this operation and remember the choice.
3074    RejectAlways,
3075    /// Custom or future permission option kind.
3076    ///
3077    /// Values beginning with `_` are reserved for implementation-specific
3078    /// extensions. Unknown values that do not begin with `_` are reserved for
3079    /// future ACP variants.
3080    #[serde(untagged)]
3081    Other(String),
3082}
3083
3084/// Response to a permission request.
3085#[serde_as]
3086#[skip_serializing_none]
3087#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3088#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3089#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
3090#[serde(rename_all = "camelCase")]
3091#[non_exhaustive]
3092pub struct RequestPermissionResponse {
3093    /// The user's decision on the permission request.
3094    pub outcome: RequestPermissionOutcome,
3095    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3096    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
3097    /// Implementations MUST NOT make assumptions about values at these keys.
3098    ///
3099    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3100    #[serde_as(deserialize_as = "DefaultOnError")]
3101    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3102    #[serde(default)]
3103    #[serde(rename = "_meta")]
3104    pub meta: Option<Meta>,
3105}
3106
3107impl RequestPermissionResponse {
3108    /// Builds [`RequestPermissionResponse`] with the required response fields set; optional fields start unset or empty.
3109    #[must_use]
3110    pub fn new(outcome: RequestPermissionOutcome) -> Self {
3111        Self {
3112            outcome,
3113            meta: None,
3114        }
3115    }
3116
3117    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3118    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
3119    /// these keys.
3120    ///
3121    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3122    #[must_use]
3123    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3124        self.meta = meta.into_option();
3125        self
3126    }
3127}
3128
3129/// The outcome of a permission request.
3130#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3132#[serde(tag = "outcome", rename_all = "snake_case")]
3133#[non_exhaustive]
3134pub enum RequestPermissionOutcome {
3135    /// The session's work was cancelled before the user responded.
3136    ///
3137    /// When a client sends a `session/cancel` notification, it MUST respond to all
3138    /// pending `session/request_permission` requests in that session with this
3139    /// `cancelled` outcome, without answering any request twice. Requests in other
3140    /// sessions are unaffected. Agents treat `cancelled` for background work as a
3141    /// denial.
3142    ///
3143    /// See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#cancellation)
3144    Cancelled(CancelledPermissionOutcome),
3145    /// The user selected one of the provided options.
3146    #[serde(rename_all = "camelCase")]
3147    Selected(SelectedPermissionOutcome),
3148    /// Custom or future permission outcome.
3149    ///
3150    /// Values beginning with `_` are reserved for implementation-specific
3151    /// extensions. Unknown values that do not begin with `_` are reserved for
3152    /// future ACP variants.
3153    ///
3154    /// Agents that do not understand this outcome MUST NOT treat it as approval.
3155    /// They should preserve the raw payload when storing, replaying, proxying, or
3156    /// forwarding permission responses, and otherwise fail or decline the
3157    /// permission request according to policy.
3158    #[serde(untagged)]
3159    Other(OtherRequestPermissionOutcome),
3160}
3161
3162/// Custom or future permission outcome payload.
3163///
3164/// This preserves the unknown `outcome` discriminator and the rest of the
3165/// outcome object for agents that store, replay, proxy, or forward permission
3166/// responses.
3167#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3168#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3169#[cfg_attr(feature = "schemars", schemars(inline))]
3170#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
3171#[serde(rename_all = "camelCase")]
3172#[non_exhaustive]
3173pub struct OtherRequestPermissionOutcome {
3174    /// Custom or future permission outcome.
3175    ///
3176    /// Values beginning with `_` are reserved for implementation-specific
3177    /// extensions. Unknown values that do not begin with `_` are reserved for
3178    /// future ACP variants.
3179    pub outcome: String,
3180    /// Additional fields from the unknown permission outcome payload.
3181    #[serde(flatten)]
3182    pub fields: BTreeMap<String, serde_json::Value>,
3183}
3184
3185impl OtherRequestPermissionOutcome {
3186    /// Builds [`OtherRequestPermissionOutcome`] from an unknown discriminator and preserves the remaining extension fields.
3187    #[must_use]
3188    pub fn new(
3189        outcome: impl Into<String>,
3190        mut fields: BTreeMap<String, serde_json::Value>,
3191    ) -> Self {
3192        fields.remove("outcome");
3193        Self {
3194            outcome: outcome.into(),
3195            fields,
3196        }
3197    }
3198}
3199
3200impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
3201    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3202    where
3203        D: serde::Deserializer<'de>,
3204    {
3205        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
3206        let outcome = fields
3207            .remove("outcome")
3208            .ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
3209        let serde_json::Value::String(outcome) = outcome else {
3210            return Err(serde::de::Error::custom("`outcome` must be a string"));
3211        };
3212
3213        if is_known_request_permission_outcome(&outcome) {
3214            return Err(serde::de::Error::custom(format!(
3215                "known request permission outcome `{outcome}` did not match its schema"
3216            )));
3217        }
3218
3219        Ok(Self { outcome, fields })
3220    }
3221}
3222
3223fn is_known_request_permission_outcome(outcome: &str) -> bool {
3224    matches!(outcome, "cancelled" | "selected")
3225}
3226
3227#[cfg(feature = "schemars")]
3228fn other_request_permission_outcome_schema(schema: &mut Schema) {
3229    super::schema_util::reject_known_string_discriminators(
3230        schema,
3231        "outcome",
3232        &["cancelled", "selected"],
3233    );
3234}
3235
3236/// The session's work was cancelled before the user responded.
3237#[serde_as]
3238#[skip_serializing_none]
3239#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3240#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3241#[serde(rename_all = "camelCase")]
3242#[non_exhaustive]
3243pub struct CancelledPermissionOutcome {
3244    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3245    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
3246    /// Implementations MUST NOT make assumptions about values at these keys.
3247    ///
3248    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3249    #[serde_as(deserialize_as = "DefaultOnError")]
3250    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3251    #[serde(default)]
3252    #[serde(rename = "_meta")]
3253    pub meta: Option<Meta>,
3254}
3255
3256impl CancelledPermissionOutcome {
3257    /// Builds an empty [`CancelledPermissionOutcome`].
3258    #[must_use]
3259    pub fn new() -> Self {
3260        Self::default()
3261    }
3262
3263    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3264    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
3265    /// these keys.
3266    ///
3267    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3268    #[must_use]
3269    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3270        self.meta = meta.into_option();
3271        self
3272    }
3273}
3274
3275impl From<CancelledPermissionOutcome> for RequestPermissionOutcome {
3276    fn from(outcome: CancelledPermissionOutcome) -> Self {
3277        Self::Cancelled(outcome)
3278    }
3279}
3280
3281/// The user selected one of the provided options.
3282#[serde_as]
3283#[skip_serializing_none]
3284#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3286#[serde(rename_all = "camelCase")]
3287#[non_exhaustive]
3288pub struct SelectedPermissionOutcome {
3289    /// The ID of the option the user selected.
3290    pub option_id: PermissionOptionId,
3291    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3292    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
3293    /// Implementations MUST NOT make assumptions about values at these keys.
3294    ///
3295    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3296    #[serde_as(deserialize_as = "DefaultOnError")]
3297    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3298    #[serde(default)]
3299    #[serde(rename = "_meta")]
3300    pub meta: Option<Meta>,
3301}
3302
3303impl SelectedPermissionOutcome {
3304    /// Builds [`SelectedPermissionOutcome`] with the required fields set; optional fields start unset or empty.
3305    #[must_use]
3306    pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
3307        Self {
3308            option_id: option_id.into(),
3309            meta: None,
3310        }
3311    }
3312
3313    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3314    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
3315    /// these keys.
3316    ///
3317    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3318    #[must_use]
3319    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3320        self.meta = meta.into_option();
3321        self
3322    }
3323}
3324
3325// Capabilities
3326
3327/// Capabilities supported by the client.
3328///
3329/// Advertised during initialization to inform the agent about
3330/// available features and methods.
3331///
3332/// See protocol docs: [Client Capabilities](https://agentclientprotocol.com/protocol/v2/initialization#client-capabilities)
3333#[serde_as]
3334#[skip_serializing_none]
3335#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3336#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3337#[serde(rename_all = "camelCase")]
3338#[non_exhaustive]
3339pub struct ClientCapabilities {
3340    /// Authentication capabilities supported by the client.
3341    /// Determines which authentication method types the agent may include
3342    /// in its `InitializeResponse`.
3343    ///
3344    /// Optional. Omitted and `null` are equivalent and mean the client does not advertise any
3345    /// authentication-method extensions.
3346    #[serde_as(deserialize_as = "DefaultOnError")]
3347    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3348    #[serde(default)]
3349    pub auth: Option<ClientAuthCapabilities>,
3350    /// Elicitation capabilities supported by the client.
3351    /// Determines which elicitation modes the agent may use.
3352    ///
3353    /// Optional. Omitted and `null` are equivalent and mean the client does not advertise
3354    /// elicitation support.
3355    #[serde_as(deserialize_as = "DefaultOnError")]
3356    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3357    #[serde(default)]
3358    pub elicitation: Option<ElicitationCapabilities>,
3359
3360    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3361    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
3362    /// Implementations MUST NOT make assumptions about values at these keys.
3363    ///
3364    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3365    #[serde_as(deserialize_as = "DefaultOnError")]
3366    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3367    #[serde(default)]
3368    #[serde(rename = "_meta")]
3369    pub meta: Option<Meta>,
3370}
3371
3372impl ClientCapabilities {
3373    /// Builds an empty [`ClientCapabilities`]; use builder methods to advertise supported sub-capabilities.
3374    #[must_use]
3375    pub fn new() -> Self {
3376        Self::default()
3377    }
3378
3379    /// Authentication capabilities supported by the client.
3380    /// Determines which authentication method types the agent may include
3381    /// in its `InitializeResponse`.
3382    #[must_use]
3383    pub fn auth(mut self, auth: impl IntoOption<ClientAuthCapabilities>) -> Self {
3384        self.auth = auth.into_option();
3385        self
3386    }
3387
3388    /// Elicitation capabilities supported by the client.
3389    /// Determines which elicitation modes the agent may use.
3390    #[must_use]
3391    pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
3392        self.elicitation = elicitation.into_option();
3393        self
3394    }
3395
3396    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3397    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
3398    /// these keys.
3399    ///
3400    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3401    #[must_use]
3402    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3403        self.meta = meta.into_option();
3404        self
3405    }
3406}
3407
3408/// Authentication capabilities supported by the client.
3409///
3410/// Advertised during initialization to inform the agent which authentication
3411/// method types the client can handle. This governs opt-in types that require
3412/// additional client-side support.
3413#[serde_as]
3414#[skip_serializing_none]
3415#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3416#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3417#[serde(rename_all = "camelCase")]
3418#[non_exhaustive]
3419pub struct ClientAuthCapabilities {
3420    /// Whether the client supports `terminal` authentication methods.
3421    ///
3422    /// Optional. Omitted and `null` are equivalent and mean the client does not advertise support.
3423    /// The client should supply `{}` only when it can reproduce the configured
3424    /// agent invocation in an interactive terminal. Supplying `{}` means the
3425    /// agent may include `terminal` entries in its authentication methods.
3426    #[serde_as(deserialize_as = "DefaultOnError")]
3427    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3428    #[serde(default)]
3429    pub terminal: Option<TerminalAuthCapabilities>,
3430    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3431    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
3432    /// Implementations MUST NOT make assumptions about values at these keys.
3433    ///
3434    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3435    #[serde_as(deserialize_as = "DefaultOnError")]
3436    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3437    #[serde(default)]
3438    #[serde(rename = "_meta")]
3439    pub meta: Option<Meta>,
3440}
3441
3442impl ClientAuthCapabilities {
3443    /// Builds an empty [`ClientAuthCapabilities`]; use builder methods to advertise supported sub-capabilities.
3444    #[must_use]
3445    pub fn new() -> Self {
3446        Self::default()
3447    }
3448
3449    /// Whether the client supports `terminal` authentication methods.
3450    ///
3451    /// Omitted and `null` are equivalent and mean the client does not advertise support.
3452    /// The client should supply `{}` only when it can reproduce the configured
3453    /// agent invocation in an interactive terminal. Supplying `{}` means the
3454    /// agent may include `AuthMethod::Terminal` entries in its authentication
3455    /// methods.
3456    #[must_use]
3457    pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
3458        self.terminal = terminal.into_option();
3459        self
3460    }
3461
3462    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3463    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
3464    /// these keys.
3465    ///
3466    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3467    #[must_use]
3468    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3469        self.meta = meta.into_option();
3470        self
3471    }
3472}
3473
3474/// Capabilities for terminal authentication methods.
3475///
3476/// Supplying `{}` means the client can reproduce the configured agent
3477/// invocation in an interactive terminal and supports terminal authentication
3478/// methods.
3479#[serde_as]
3480#[skip_serializing_none]
3481#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3482#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3483#[non_exhaustive]
3484pub struct TerminalAuthCapabilities {
3485    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3486    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
3487    /// Implementations MUST NOT make assumptions about values at these keys.
3488    ///
3489    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3490    #[serde_as(deserialize_as = "DefaultOnError")]
3491    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3492    #[serde(default)]
3493    #[serde(rename = "_meta")]
3494    pub meta: Option<Meta>,
3495}
3496
3497impl TerminalAuthCapabilities {
3498    /// Builds an empty [`TerminalAuthCapabilities`]; use builder methods to advertise supported sub-capabilities.
3499    #[must_use]
3500    pub fn new() -> Self {
3501        Self::default()
3502    }
3503
3504    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
3505    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
3506    /// these keys.
3507    ///
3508    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3509    #[must_use]
3510    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3511        self.meta = meta.into_option();
3512        self
3513    }
3514}
3515
3516// Method schema
3517
3518/// Names of all methods that clients handle.
3519///
3520/// Provides a centralized definition of method names used in the protocol.
3521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3522#[non_exhaustive]
3523pub struct ClientMethodNames {
3524    /// Method for requesting permission from the user.
3525    pub session_request_permission: &'static str,
3526    /// Notification for session updates.
3527    pub session_update: &'static str,
3528    /// Method for exchanging MCP-over-ACP messages.
3529    #[cfg(feature = "unstable_mcp_over_acp")]
3530    pub mcp_message: &'static str,
3531    /// Method for elicitation.
3532    pub elicitation_create: &'static str,
3533    /// Notification for elicitation completion.
3534    pub elicitation_complete: &'static str,
3535}
3536
3537/// Constant containing all client method names.
3538pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
3539    session_update: SESSION_UPDATE_NOTIFICATION,
3540    session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
3541    #[cfg(feature = "unstable_mcp_over_acp")]
3542    mcp_message: MCP_MESSAGE_METHOD_NAME,
3543    elicitation_create: ELICITATION_CREATE_METHOD_NAME,
3544    elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
3545};
3546
3547/// Notification name for session updates.
3548pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
3549/// Method name for requesting user permission.
3550pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
3551/// Method name for elicitation.
3552pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
3553/// Notification name for elicitation completion.
3554pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
3555
3556/// All possible requests that an agent can send to a client.
3557///
3558/// This enum is used internally for routing RPC requests. You typically won't need
3559/// to use this directly.
3560///
3561/// This enum encompasses all method calls from agent to client.
3562#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3563#[derive(Clone, Debug, Serialize, Deserialize)]
3564#[serde(untagged)]
3565#[cfg_attr(feature = "schemars", schemars(inline))]
3566#[non_exhaustive]
3567pub enum AgentRequest {
3568    /// Requests permission from the user for an operation.
3569    ///
3570    /// Called by the agent when it needs user authorization before executing
3571    /// a potentially sensitive operation. The client should present the options
3572    /// to the user and return their decision.
3573    ///
3574    /// If the client cancels active session work via `session/cancel`, it MUST
3575    /// respond to this request with the `cancelled` outcome.
3576    ///
3577    /// See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/tool-calls#requesting-permission)
3578    RequestPermissionRequest(Box<RequestPermissionRequest>),
3579    /// Requests structured user input via a form or URL.
3580    ///
3581    /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/v2/elicitation)
3582    CreateElicitationRequest(Box<CreateElicitationRequest>),
3583    /// **UNSTABLE**
3584    ///
3585    /// This capability is not part of the spec yet, and may be removed or changed at any point.
3586    ///
3587    /// Exchanges an MCP-over-ACP message.
3588    #[cfg(feature = "unstable_mcp_over_acp")]
3589    MessageMcpRequest(Box<MessageMcpRequest>),
3590    /// Handles extension method requests from the agent.
3591    ///
3592    /// Allows the Agent to send an arbitrary request that is not part of the ACP spec.
3593    /// Extension methods provide a way to add custom functionality while maintaining
3594    /// protocol compatibility.
3595    ///
3596    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3597    ExtMethodRequest(Box<ExtRequest>),
3598}
3599
3600impl AgentRequest {
3601    /// Returns the corresponding method name of the request.
3602    #[must_use]
3603    pub fn method(&self) -> &str {
3604        match self {
3605            Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
3606            Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
3607            #[cfg(feature = "unstable_mcp_over_acp")]
3608            Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
3609            Self::ExtMethodRequest(ext_request) => &ext_request.method,
3610        }
3611    }
3612}
3613
3614/// All possible responses that a client can send to an agent.
3615///
3616/// This enum is used internally for routing RPC responses. You typically won't need
3617/// to use this directly - the responses are handled automatically by the connection.
3618///
3619/// These are responses to the corresponding `AgentRequest` variants.
3620#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3621#[derive(Clone, Debug, Serialize, Deserialize)]
3622#[serde(untagged)]
3623#[cfg_attr(feature = "schemars", schemars(inline))]
3624#[non_exhaustive]
3625pub enum ClientResponse {
3626    /// Successful result returned for a `session/request_permission` request.
3627    RequestPermissionResponse(Box<RequestPermissionResponse>),
3628    /// Successful result returned for a `elicitation/create` request.
3629    CreateElicitationResponse(Box<CreateElicitationResponse>),
3630    /// Successful result returned by an MCP-over-ACP `mcp/message` request.
3631    #[cfg(feature = "unstable_mcp_over_acp")]
3632    MessageMcpResponse(Box<MessageMcpResponse>),
3633    /// Successful result returned by an extension method outside the core ACP method set.
3634    ExtMethodResponse(Box<ExtResponse>),
3635}
3636
3637/// All possible notifications that an agent can send to a client.
3638///
3639/// This enum is used internally for routing RPC notifications. You typically won't need
3640/// to use this directly.
3641///
3642/// Notifications do not expect a response.
3643#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3644#[derive(Clone, Debug, Serialize, Deserialize)]
3645#[serde(untagged)]
3646#[cfg_attr(feature = "schemars", schemars(inline))]
3647#[non_exhaustive]
3648pub enum AgentNotification {
3649    /// Handles session update notifications from the agent.
3650    ///
3651    /// This is a notification endpoint (no response expected) that receives
3652    /// updates about session activity, including message updates, message chunks,
3653    /// tool calls, and execution plans.
3654    ///
3655    /// Note: Clients SHOULD continue accepting tool call updates even after
3656    /// sending a `session/cancel` notification, as the agent may send final
3657    /// updates before reporting an idle `state_update` with the cancelled
3658    /// stop reason.
3659    ///
3660    /// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#3-agent-reports-output)
3661    UpdateSessionNotification(Box<UpdateSessionNotification>),
3662    /// Notification that a URL-based elicitation has completed.
3663    ///
3664    /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/v2/elicitation#url-completion)
3665    CompleteElicitationNotification(Box<CompleteElicitationNotification>),
3666    /// Handles extension notifications from the agent.
3667    ///
3668    /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec.
3669    /// Extension notifications provide a way to send one-way messages for custom functionality
3670    /// while maintaining protocol compatibility.
3671    ///
3672    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
3673    ExtNotification(Box<ExtNotification>),
3674}
3675
3676impl AgentNotification {
3677    /// Returns the corresponding method name of the notification.
3678    #[must_use]
3679    pub fn method(&self) -> &str {
3680        match self {
3681            Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
3682            Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
3683            Self::ExtNotification(ext_notification) => &ext_notification.method,
3684        }
3685    }
3686}
3687
3688#[cfg(test)]
3689mod tests {
3690
3691    #[test]
3692    fn replacement_lists_reject_non_arrays_instead_of_clearing() {
3693        use serde_json::json;
3694        for (update, key) in [
3695            ("config_option_update", "configOptions"),
3696            ("available_commands_update", "availableCommands"),
3697        ] {
3698            let empty = json!({ "sessionUpdate": update, key: [] });
3699            serde_json::from_value::<super::SessionUpdate>(empty).unwrap();
3700            for bad in [json!("oops"), json!(null), json!({})] {
3701                let wire = json!({ "sessionUpdate": update, key: bad });
3702                assert!(
3703                    serde_json::from_value::<super::SessionUpdate>(wire).is_err(),
3704                    "{update} {bad}"
3705                );
3706            }
3707        }
3708        let plan = |entries: serde_json::Value| {
3709            json!({ "sessionUpdate": "plan_update",
3710                "plan": { "type": "items", "planId": "p", "entries": entries } })
3711        };
3712        serde_json::from_value::<super::SessionUpdate>(plan(json!([]))).unwrap();
3713        assert!(serde_json::from_value::<super::SessionUpdate>(plan(json!("oops"))).is_err());
3714        // Invalid items are still skipped.
3715        let skipped =
3716            plan(json!([42, { "content": "a", "priority": "high", "status": "pending" }]));
3717        let super::SessionUpdate::PlanUpdate(update) =
3718            serde_json::from_value::<super::SessionUpdate>(skipped).unwrap()
3719        else {
3720            panic!("expected plan_update");
3721        };
3722        let crate::v2::PlanUpdateContent::Items(items) = update.plan else {
3723            panic!("expected items");
3724        };
3725        assert_eq!(items.entries.len(), 1);
3726    }
3727
3728    #[test]
3729    fn other_available_command_preserves_meta_verbatim() {
3730        for meta in [serde_json::json!({ "k": 1 }), serde_json::json!("bad")] {
3731            let wire = serde_json::json!({ "type": "_custom", "name": "c",
3732                "description": "d", "_meta": meta, "extra": true });
3733            let command: super::AvailableCommand = serde_json::from_value(wire.clone()).unwrap();
3734            assert_eq!(command.meta(), meta.as_object());
3735            assert_eq!(serde_json::to_value(&command).unwrap(), wire);
3736        }
3737    }
3738
3739    #[test]
3740    fn cancelled_permission_outcome_wire_format() {
3741        let outcome: RequestPermissionOutcome =
3742            serde_json::from_value(serde_json::json!({ "outcome": "cancelled" })).unwrap();
3743        assert_eq!(outcome, CancelledPermissionOutcome::new().into());
3744        assert_eq!(
3745            serde_json::to_value(&outcome).unwrap(),
3746            serde_json::json!({ "outcome": "cancelled" })
3747        );
3748
3749        let with_meta: RequestPermissionOutcome = CancelledPermissionOutcome::new()
3750            .meta(serde_json::Map::from_iter([(
3751                "k".to_string(),
3752                serde_json::json!(1),
3753            )]))
3754            .into();
3755        let json = serde_json::to_value(&with_meta).unwrap();
3756        assert_eq!(
3757            json,
3758            serde_json::json!({ "outcome": "cancelled", "_meta": { "k": 1 } })
3759        );
3760        assert_eq!(
3761            serde_json::from_value::<RequestPermissionOutcome>(json).unwrap(),
3762            with_meta
3763        );
3764    }
3765
3766    use super::*;
3767
3768    #[cfg(feature = "unstable_subagents")]
3769    #[test]
3770    fn subagent_update_serializes_as_upsert() {
3771        use serde_json::json;
3772
3773        let announced =
3774            SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
3775                SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
3776            ));
3777        let wire = json!({
3778            "sessionUpdate": "subagent_update",
3779            "sessionId": "sess_child_1",
3780            "capabilities": { "cancel": {} }
3781        });
3782        assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
3783        assert_eq!(
3784            serde_json::from_value::<SessionUpdate>(wire).unwrap(),
3785            announced
3786        );
3787
3788        let minimal = SubagentUpdate::new("sess_child_1");
3789        assert_eq!(
3790            serde_json::to_value(&minimal).unwrap(),
3791            json!({
3792                "sessionId": "sess_child_1"
3793            })
3794        );
3795        assert!(minimal.capabilities.is_undefined());
3796        assert!(minimal.state.is_undefined());
3797        assert!(minimal.meta.is_undefined());
3798
3799        // Patch semantics distinguish omitted fields from explicit nulls.
3800        let cleared_wire = json!({
3801            "sessionId": "sess_child_1",
3802            "capabilities": null,
3803            "state": null,
3804            "_meta": null
3805        });
3806        let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3807        assert!(cleared.capabilities.is_null());
3808        assert!(cleared.state.is_null());
3809        assert!(cleared.meta.is_null());
3810        assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3811
3812        let revoked: SubagentUpdate = serde_json::from_value(json!({
3813            "sessionId": "sess_child_1",
3814            "capabilities": {}
3815        }))
3816        .unwrap();
3817        assert_eq!(
3818            revoked.capabilities,
3819            MaybeUndefined::Value(SubagentSessionCapabilities::new())
3820        );
3821        assert!(
3822            matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
3823        );
3824
3825        assert!(
3826            serde_json::from_value::<SessionUpdate>(json!({
3827                "sessionUpdate": "subagent_update"
3828            }))
3829            .is_err()
3830        );
3831    }
3832
3833    #[cfg(feature = "unstable_subagents")]
3834    #[test]
3835    fn subagent_cancel_capability_is_optional_object() {
3836        use serde_json::json;
3837
3838        for wire in [
3839            json!({}),
3840            json!({"cancel": null}),
3841            json!({"cancel": true}),
3842            json!({"cancel": false}),
3843        ] {
3844            let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
3845            assert!(child.cancel.is_none());
3846            assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
3847        }
3848        let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
3849        assert_eq!(
3850            serde_json::to_value(&enabled).unwrap(),
3851            json!({"cancel": {}})
3852        );
3853        assert_eq!(
3854            serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
3855            enabled
3856        );
3857        let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
3858        let with_meta =
3859            SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
3860        let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
3861        assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
3862        assert_eq!(
3863            serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
3864            with_meta
3865        );
3866        assert!(enabled.cancel.is_some());
3867        assert!(enabled.cancel(None).cancel.is_none());
3868        let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
3869        assert_eq!(
3870            serde_json::to_value(removed).unwrap(),
3871            json!({"sessionId": "child", "capabilities": {}})
3872        );
3873    }
3874
3875    #[cfg(feature = "unstable_subagents")]
3876    #[test]
3877    fn subagent_display_metadata_uses_patch_semantics() {
3878        use serde_json::json;
3879
3880        let update = SubagentUpdate::new("child")
3881            .title("Test investigator".to_string())
3882            .description("Investigates platform-specific test failures.".to_string());
3883        let wire = json!({
3884            "sessionId": "child",
3885            "title": "Test investigator",
3886            "description": "Investigates platform-specific test failures."
3887        });
3888        assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3889        assert_eq!(
3890            serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
3891            update
3892        );
3893
3894        let minimal = SubagentUpdate::new("child");
3895        assert!(minimal.title.is_undefined());
3896        assert!(minimal.description.is_undefined());
3897        assert_eq!(
3898            serde_json::to_value(&minimal).unwrap(),
3899            json!({"sessionId": "child"})
3900        );
3901        let cleared_wire = json!({
3902            "sessionId": "child", "title": null, "description": null
3903        });
3904        let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3905        assert!(cleared.title.is_null());
3906        assert!(cleared.description.is_null());
3907        assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3908
3909        let title_only: SubagentUpdate = serde_json::from_value(json!({
3910            "sessionId": "child", "title": "Updated title"
3911        }))
3912        .unwrap();
3913        assert_eq!(
3914            title_only.title,
3915            MaybeUndefined::Value("Updated title".to_string())
3916        );
3917        assert!(title_only.description.is_undefined());
3918        let malformed: SubagentUpdate = serde_json::from_value(json!({
3919            "sessionId": "child", "title": false, "description": false
3920        }))
3921        .unwrap();
3922        assert_eq!(malformed, minimal);
3923    }
3924
3925    #[cfg(feature = "unstable_subagents")]
3926    #[test]
3927    fn subagent_notification_keeps_parent_and_child_ids_nested() {
3928        use serde_json::json;
3929
3930        let notification = UpdateSessionNotification::new(
3931            "parent",
3932            SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
3933        );
3934        let wire = json!({
3935            "sessionId": "parent",
3936            "update": {
3937                "sessionUpdate": "subagent_update",
3938                "sessionId": "child"
3939            }
3940        });
3941        assert_eq!(serde_json::to_value(&notification).unwrap(), wire);
3942        assert_eq!(
3943            serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
3944            notification
3945        );
3946        for malformed in [
3947            json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
3948            json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
3949        ] {
3950            assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
3951        }
3952    }
3953
3954    #[cfg(feature = "unstable_subagents")]
3955    #[test]
3956    fn subagent_mirrors_normal_child_state_notifications() {
3957        use serde_json::json;
3958
3959        let association = SubagentUpdate::new("sess_child");
3960        for state in [
3961            StateUpdate::Running(RunningStateUpdate::new()),
3962            StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
3963            StateUpdate::Unknown(UnknownStateUpdate::new()),
3964            StateUpdate::Running(RunningStateUpdate::new()),
3965            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
3966            StateUpdate::Running(RunningStateUpdate::new()),
3967            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
3968        ] {
3969            let child_notification = UpdateSessionNotification::new(
3970                association.session_id.clone(),
3971                SessionUpdate::StateUpdate(state.clone()),
3972            );
3973            let parent_notification = UpdateSessionNotification::new(
3974                "sess_parent",
3975                SessionUpdate::SubagentUpdate(association.clone().state(state)),
3976            );
3977            let child_wire = serde_json::to_value(&child_notification).unwrap();
3978            let parent_wire = serde_json::to_value(&parent_notification).unwrap();
3979            assert_eq!(child_wire["sessionId"], json!("sess_child"));
3980            assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
3981            assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
3982            assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
3983            let mut child_snapshot = child_wire["update"].clone();
3984            child_snapshot
3985                .as_object_mut()
3986                .unwrap()
3987                .remove("sessionUpdate");
3988            assert_eq!(parent_wire["update"]["state"], child_snapshot);
3989            assert_eq!(
3990                serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
3991                child_notification
3992            );
3993            assert_eq!(
3994                serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
3995                parent_notification
3996            );
3997        }
3998    }
3999
4000    #[cfg(feature = "unstable_subagents")]
4001    #[test]
4002    fn subagent_state_uses_whole_snapshot_patch_semantics() {
4003        use serde_json::json;
4004
4005        let minimal = SubagentUpdate::new("child");
4006        assert!(minimal.state.is_undefined());
4007        assert_eq!(
4008            serde_json::to_value(&minimal).unwrap(),
4009            json!({"sessionId": "child"})
4010        );
4011
4012        let cleared = minimal.clone().state(None::<StateUpdate>);
4013        assert!(cleared.state.is_null());
4014        assert_eq!(
4015            serde_json::to_value(&cleared).unwrap(),
4016            json!({"sessionId": "child", "state": null})
4017        );
4018        assert_eq!(
4019            serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
4020                .unwrap(),
4021            cleared
4022        );
4023
4024        for snapshot in [
4025            json!({"state": "running", "_meta": {"source": "worker"}}),
4026            json!({"state": "idle", "stopReason": "end_turn"}),
4027            json!({"state": "requires_action"}),
4028            json!({"state": "unknown"}),
4029            json!({"state": "_custom", "detail": {"phase": 2}}),
4030        ] {
4031            let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
4032            let update = minimal.clone().state(state.clone());
4033            let wire = json!({"sessionId": "child", "state": snapshot});
4034            assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4035            assert_eq!(
4036                serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4037                update
4038            );
4039            assert_eq!(update.state, MaybeUndefined::Value(state));
4040        }
4041
4042        // Each concrete value is the entire replacement snapshot, not a merge
4043        // with a previous state's stop reason or metadata.
4044        let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
4045            IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
4046        ));
4047        let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
4048        assert_eq!(
4049            serde_json::to_value(running).unwrap(),
4050            json!({"sessionId": "child", "state": {"state": "running"}})
4051        );
4052        for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
4053            let parsed: SubagentUpdate =
4054                serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
4055            assert_eq!(parsed, minimal);
4056        }
4057    }
4058
4059    #[cfg(feature = "unstable_subagents")]
4060    #[test]
4061    fn unknown_activity_is_a_known_state_update() {
4062        use serde_json::json;
4063
4064        let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
4065            UnknownStateUpdate::new().meta(
4066                [("source".into(), json!("worker"))]
4067                    .into_iter()
4068                    .collect::<Meta>(),
4069            ),
4070        ));
4071        let wire = json!({
4072            "sessionUpdate": "state_update",
4073            "state": "unknown",
4074            "_meta": { "source": "worker" }
4075        });
4076        assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4077        assert_eq!(
4078            serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4079            update
4080        );
4081
4082        for meta in [json!(null), json!(false)] {
4083            let state: StateUpdate = serde_json::from_value(json!({
4084                "state": "unknown",
4085                "_meta": meta
4086            }))
4087            .unwrap();
4088            assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
4089            assert_eq!(
4090                serde_json::to_value(state).unwrap(),
4091                json!({"state": "unknown"})
4092            );
4093        }
4094        assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
4095    }
4096
4097    #[cfg(not(feature = "unstable_subagents"))]
4098    #[test]
4099    fn unknown_activity_is_preserved_without_subagents_feature() {
4100        let wire = serde_json::json!({
4101            "sessionUpdate": "state_update",
4102            "state": "unknown",
4103            "_meta": { "source": "worker" }
4104        });
4105        let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4106        let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
4107            panic!("expected unrecognized state payload");
4108        };
4109        assert_eq!(state.state, "unknown");
4110        assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4111    }
4112
4113    #[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
4114    #[test]
4115    fn subagent_schema_carries_optional_nullable_state_snapshot() {
4116        use serde_json::json;
4117
4118        let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
4119        let properties = schema["properties"].as_object().unwrap();
4120        assert!(properties.contains_key("sessionId"));
4121        assert!(properties.contains_key("title"));
4122        assert!(properties.contains_key("description"));
4123        assert!(properties.contains_key("capabilities"));
4124        assert!(properties.contains_key("state"));
4125        assert!(properties.contains_key("_meta"));
4126        assert!(!properties.contains_key("name"));
4127        assert!(!properties.contains_key("task"));
4128        assert_eq!(schema["required"], json!(["sessionId"]));
4129        assert_eq!(
4130            properties["state"]["x-deserialize-default-on-error"],
4131            json!(true)
4132        );
4133        let state_variants = properties["state"]["anyOf"].as_array().unwrap();
4134        assert!(state_variants.contains(&json!({"type": "null"})));
4135        assert!(
4136            state_variants
4137                .iter()
4138                .any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
4139        );
4140        let child =
4141            serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
4142        let variants = child["properties"]["cancel"]["anyOf"]
4143            .as_array()
4144            .expect("cancel must allow the capability object or null");
4145        assert!(variants.contains(&json!({"type": "null"})));
4146        assert!(
4147            variants
4148                .iter()
4149                .any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
4150        );
4151        assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
4152        let cancel =
4153            serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
4154        assert_eq!(cancel["type"], "object");
4155        assert!(
4156            !child["required"]
4157                .as_array()
4158                .is_some_and(|required| required.contains(&json!("cancel")))
4159        );
4160    }
4161
4162    #[cfg(not(feature = "unstable_subagents"))]
4163    #[test]
4164    fn unsupported_subagent_update_is_preserved() {
4165        let wire = serde_json::json!({
4166            "sessionUpdate": "subagent_update",
4167            "sessionId": "sess_child",
4168            "capabilities": { "cancel": {} }
4169        });
4170        let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4171        assert!(matches!(&parsed, SessionUpdate::Other(_)));
4172        assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4173    }
4174
4175    #[test]
4176    fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
4177        use serde_json::json;
4178
4179        let mut meta = Meta::new();
4180        meta.insert("source".into(), json!("fallback"));
4181        let v2_notice = SessionUpdate::Notice(
4182            Notice::new(NoticeSeverity::Error, "Provider degraded")
4183                .description("Requests may take longer than usual.")
4184                .meta(meta.clone()),
4185        );
4186        let expected = json!({
4187            "sessionUpdate": "notice",
4188            "severity": "error",
4189            "title": "Provider degraded",
4190            "description": "Requests may take longer than usual.",
4191            "_meta": { "source": "fallback" }
4192        });
4193        assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
4194
4195        let v1_notice = crate::v1::SessionUpdate::Notice(
4196            crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
4197                .description("Requests may take longer than usual.")
4198                .meta(meta),
4199        );
4200        assert_eq!(
4201            serde_json::to_value(v2_notice).unwrap(),
4202            serde_json::to_value(v1_notice).unwrap()
4203        );
4204
4205        let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
4206            "sessionUpdate": "notice",
4207            "severity": "critical",
4208            "title": "Provider degraded",
4209            "description": null,
4210            "_meta": null
4211        }))
4212        .unwrap() else {
4213            panic!("expected notice");
4214        };
4215
4216        assert_eq!(
4217            notice.severity,
4218            NoticeSeverity::Other("critical".to_string())
4219        );
4220        assert_eq!(notice.description, None);
4221        assert_eq!(notice.meta, None);
4222        assert_eq!(
4223            serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
4224            json!({
4225                "sessionUpdate": "notice",
4226                "severity": "critical",
4227                "title": "Provider degraded"
4228            })
4229        );
4230    }
4231
4232    #[test]
4233    fn malformed_known_notice_is_not_hidden_as_unknown() {
4234        use serde_json::json;
4235
4236        for malformed in [
4237            json!({
4238                "sessionUpdate": "notice",
4239                "severity": "warning",
4240                "title": 42
4241            }),
4242            json!({
4243                "sessionUpdate": "notice",
4244                "severity": 42,
4245                "title": "MCP server unavailable"
4246            }),
4247            json!({
4248                "sessionUpdate": "notice",
4249                "severity": "warning"
4250            }),
4251            json!({
4252                "sessionUpdate": "notice",
4253                "severity": "warning",
4254                "title": null
4255            }),
4256            json!({
4257                "sessionUpdate": "notice",
4258                "title": "MCP server unavailable"
4259            }),
4260            json!({
4261                "sessionUpdate": "notice",
4262                "severity": null,
4263                "title": "MCP server unavailable"
4264            }),
4265        ] {
4266            assert!(serde_json::from_str::<SessionUpdate>(&malformed.to_string()).is_err());
4267            assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
4268        }
4269    }
4270
4271    #[test]
4272    fn notice_tolerates_invalid_optional_display_fields_and_custom_severity() {
4273        use serde_json::json;
4274
4275        let wire = json!({
4276            "sessionUpdate": "notice",
4277            "severity": "_custom",
4278            "title": "Provider degraded",
4279            "description": 42,
4280            "_meta": false
4281        });
4282        let SessionUpdate::Notice(notice) = serde_json::from_value(wire.clone()).unwrap() else {
4283            panic!("expected notice");
4284        };
4285        assert_eq!(
4286            serde_json::from_str::<SessionUpdate>(&wire.to_string()).unwrap(),
4287            SessionUpdate::Notice(notice.clone())
4288        );
4289        assert_eq!(notice.severity, NoticeSeverity::Other("_custom".into()));
4290        assert_eq!(notice.description, None);
4291        assert_eq!(notice.meta, None);
4292        assert_eq!(
4293            serde_json::to_value(notice).unwrap(),
4294            json!({"severity": "_custom", "title": "Provider degraded"})
4295        );
4296    }
4297
4298    #[cfg(feature = "schemars")]
4299    #[test]
4300    fn notice_schema_preserves_required_fields_and_excludes_unknown_fallback() {
4301        use serde_json::json;
4302
4303        let schema = serde_json::to_value(schemars::schema_for!(Notice)).unwrap();
4304        assert_eq!(schema["required"], json!(["severity", "title"]));
4305        assert_eq!(schema["properties"]["title"]["minLength"], 1);
4306        for field in ["description", "_meta"] {
4307            assert!(
4308                schema["properties"][field]["type"]
4309                    .as_array()
4310                    .unwrap()
4311                    .contains(&json!("null"))
4312            );
4313        }
4314        let fallback = serde_json::to_value(schemars::schema_for!(OtherSessionUpdate)).unwrap();
4315        assert!(
4316            fallback["not"]["anyOf"]
4317                .as_array()
4318                .unwrap()
4319                .iter()
4320                .any(|variant| variant["properties"]["sessionUpdate"]["const"] == "notice")
4321        );
4322    }
4323
4324    #[test]
4325    fn compaction_updates_preserve_patch_and_open_status_semantics() {
4326        use serde_json::json;
4327
4328        assert_eq!(
4329            serde_json::to_value(SessionUpdate::CompactionUpdate(
4330                CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
4331                    ContentBlock::Text(crate::v2::TextContent::new("retained")),
4332                ]),
4333            ))
4334            .unwrap(),
4335            json!({
4336                "sessionUpdate": "compaction_update",
4337                "compactionId": "cmp_001",
4338                "status": "completed",
4339                "summary": [{ "type": "text", "text": "retained" }]
4340            })
4341        );
4342
4343        let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
4344            "sessionUpdate": "compaction_update",
4345            "compactionId": "cmp_001",
4346            "status": "paused",
4347            "summary": null
4348        }))
4349        .unwrap() else {
4350            panic!("expected compaction update");
4351        };
4352        assert_eq!(update.status, CompactionStatus::Other("paused".into()));
4353        assert!(update.summary.is_null());
4354        assert!(update.error.is_undefined());
4355    }
4356
4357    #[test]
4358    fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
4359        use serde_json::json;
4360
4361        assert!(
4362            serde_json::from_value::<SessionUpdate>(json!({
4363                "sessionUpdate": "compaction_update",
4364                "status": "completed"
4365            }))
4366            .is_err()
4367        );
4368        assert!(
4369            serde_json::from_value::<SessionUpdate>(json!({
4370                "sessionUpdate": "compaction_summary_chunk",
4371                "compactionId": "cmp_001"
4372            }))
4373            .is_err()
4374        );
4375    }
4376
4377    #[test]
4378    fn test_elicitation_capability_semantics() {
4379        use serde_json::json;
4380
4381        let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
4382        assert!(unsupported.elicitation.is_none());
4383
4384        let null: ClientCapabilities =
4385            serde_json::from_value(json!({ "elicitation": null })).unwrap();
4386        assert!(null.elicitation.is_none());
4387
4388        let malformed: ClientCapabilities =
4389            serde_json::from_value(json!({ "elicitation": false })).unwrap();
4390        assert!(malformed.elicitation.is_none());
4391
4392        let empty: ClientCapabilities =
4393            serde_json::from_value(json!({ "elicitation": {} })).unwrap();
4394        let empty = empty.elicitation.expect("present capability");
4395        assert!(!empty.supports_form());
4396        assert!(!empty.supports_url());
4397
4398        let form_only: ClientCapabilities = serde_json::from_value(json!({
4399            "elicitation": { "form": {} }
4400        }))
4401        .unwrap();
4402        let form_only = form_only.elicitation.expect("advertised capability");
4403        assert!(form_only.supports_form());
4404        assert!(!form_only.supports_url());
4405
4406        let url_only: ClientCapabilities = serde_json::from_value(json!({
4407            "elicitation": { "url": {} }
4408        }))
4409        .unwrap();
4410        let url_only = url_only.elicitation.expect("advertised capability");
4411        assert!(!url_only.supports_form());
4412        assert!(url_only.supports_url());
4413
4414        let both: ClientCapabilities = serde_json::from_value(json!({
4415            "elicitation": { "form": {}, "url": {} }
4416        }))
4417        .unwrap();
4418        let both = both.elicitation.expect("advertised capability");
4419        assert!(both.supports_form());
4420        assert!(both.supports_url());
4421    }
4422
4423    #[test]
4424    fn test_elicitation_method_routing_and_envelopes() {
4425        use serde_json::json;
4426
4427        assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
4428        assert_eq!(
4429            CLIENT_METHOD_NAMES.elicitation_complete,
4430            "elicitation/complete"
4431        );
4432
4433        let request =
4434            AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
4435                crate::v2::ElicitationFormMode::new(
4436                    crate::v2::ElicitationSessionScope::new("sess_1"),
4437                    crate::v2::ElicitationSchema::new(),
4438                ),
4439                "Choose a value",
4440            )));
4441        assert_eq!(request.method(), "elicitation/create");
4442        let method = Arc::from(request.method());
4443        let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
4444            id: crate::v2::RequestId::Number(7),
4445            method,
4446            params: Some(request),
4447        });
4448        assert_eq!(
4449            serde_json::to_value(request).unwrap(),
4450            json!({
4451                "jsonrpc": "2.0",
4452                "id": 7,
4453                "method": "elicitation/create",
4454                "params": {
4455                    "mode": "form",
4456                    "sessionId": "sess_1",
4457                    "message": "Choose a value",
4458                    "requestedSchema": { "type": "object", "properties": {} }
4459                }
4460            })
4461        );
4462
4463        let notification = AgentNotification::CompleteElicitationNotification(Box::new(
4464            CompleteElicitationNotification::new("elic_1"),
4465        ));
4466        assert_eq!(notification.method(), "elicitation/complete");
4467        let method = Arc::from(notification.method());
4468        let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
4469            method,
4470            params: Some(notification),
4471        });
4472        assert_eq!(
4473            serde_json::to_value(notification).unwrap(),
4474            json!({
4475                "jsonrpc": "2.0",
4476                "method": "elicitation/complete",
4477                "params": { "elicitationId": "elic_1" }
4478            })
4479        );
4480    }
4481
4482    #[test]
4483    fn test_client_capabilities_auth_defaults_on_malformed_value() {
4484        use serde_json::json;
4485
4486        let capabilities: ClientCapabilities = serde_json::from_value(json!({
4487            "auth": false
4488        }))
4489        .unwrap();
4490
4491        assert_eq!(capabilities.auth, None);
4492    }
4493
4494    #[test]
4495    fn test_serialization_behavior() {
4496        use serde_json::json;
4497
4498        assert_eq!(
4499            serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
4500            SessionInfoUpdate {
4501                title: MaybeUndefined::Undefined,
4502                updated_at: MaybeUndefined::Undefined,
4503                meta: MaybeUndefined::Undefined
4504            }
4505        );
4506        assert_eq!(
4507            serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
4508                .unwrap(),
4509            SessionInfoUpdate {
4510                title: MaybeUndefined::Null,
4511                updated_at: MaybeUndefined::Null,
4512                meta: MaybeUndefined::Undefined
4513            }
4514        );
4515        assert_eq!(
4516            serde_json::from_value::<SessionInfoUpdate>(
4517                json!({"title": "title", "updatedAt": "timestamp"})
4518            )
4519            .unwrap(),
4520            SessionInfoUpdate {
4521                title: MaybeUndefined::Value("title".to_string()),
4522                updated_at: MaybeUndefined::Value("timestamp".to_string()),
4523                meta: MaybeUndefined::Undefined
4524            }
4525        );
4526
4527        let clear_meta =
4528            serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
4529        assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4530
4531        let mut meta = Meta::new();
4532        meta.insert("source".to_string(), json!("session-info"));
4533
4534        assert_eq!(
4535            serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
4536                "source": "session-info"
4537            }}))
4538            .unwrap()
4539            .meta,
4540            MaybeUndefined::Value(meta.clone())
4541        );
4542
4543        assert_eq!(
4544            serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
4545            json!({})
4546        );
4547
4548        assert_eq!(
4549            serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
4550            json!({"_meta": null})
4551        );
4552
4553        assert_eq!(
4554            serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
4555            json!({"_meta": {
4556                "source": "session-info"
4557            }})
4558        );
4559        assert_eq!(
4560            serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
4561            json!({"title": "title"})
4562        );
4563        assert_eq!(
4564            serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
4565            json!({"title": null})
4566        );
4567        assert_eq!(
4568            serde_json::to_value(
4569                SessionInfoUpdate::new()
4570                    .title("title")
4571                    .title(MaybeUndefined::Undefined)
4572            )
4573            .unwrap(),
4574            json!({})
4575        );
4576    }
4577
4578    #[test]
4579    fn test_content_chunk_message_id_serialization() {
4580        use serde_json::json;
4581
4582        assert_eq!(
4583            serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
4584                ContentBlock::Text(crate::v2::TextContent::new("Hello")),
4585                "msg_agent_c42b9",
4586            )))
4587            .unwrap(),
4588            json!({
4589                "sessionUpdate": "agent_message_chunk",
4590                "messageId": "msg_agent_c42b9",
4591                "content": {
4592                    "type": "text",
4593                    "text": "Hello"
4594                }
4595            })
4596        );
4597
4598        let err = serde_json::from_value::<ContentChunk>(json!({
4599            "content": {
4600                "type": "text",
4601                "text": "Hello"
4602            }
4603        }))
4604        .unwrap_err();
4605
4606        assert!(err.to_string().contains("messageId"), "{err}");
4607    }
4608
4609    #[test]
4610    fn test_tool_call_content_chunk_serialization() {
4611        use serde_json::json;
4612
4613        assert_eq!(
4614            serde_json::to_value(SessionUpdate::ToolCallContentChunk(
4615                ToolCallContentChunk::new(
4616                    "call_001",
4617                    crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
4618                )
4619            ))
4620            .unwrap(),
4621            json!({
4622                "sessionUpdate": "tool_call_content_chunk",
4623                "toolCallId": "call_001",
4624                "content": {
4625                    "type": "content",
4626                    "content": {
4627                        "type": "text",
4628                        "text": "partial output"
4629                    }
4630                }
4631            })
4632        );
4633
4634        let err = serde_json::from_value::<ToolCallContentChunk>(json!({
4635            "content": {
4636                "type": "content",
4637                "content": {
4638                    "type": "text",
4639                    "text": "partial output"
4640                }
4641            }
4642        }))
4643        .unwrap_err();
4644
4645        assert!(err.to_string().contains("toolCallId"), "{err}");
4646    }
4647
4648    #[test]
4649    fn test_full_message_serialization() {
4650        use serde_json::json;
4651
4652        assert_eq!(
4653            serde_json::to_value(SessionUpdate::UserMessage(
4654                UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
4655                    crate::v2::TextContent::new("Hello")
4656                )])
4657            ))
4658            .unwrap(),
4659            json!({
4660                "sessionUpdate": "user_message",
4661                "messageId": "msg_user_8f7a1",
4662                "content": [
4663                    {
4664                        "type": "text",
4665                        "text": "Hello"
4666                    }
4667                ]
4668            })
4669        );
4670
4671        assert_eq!(
4672            serde_json::to_value(SessionUpdate::AgentMessage(
4673                AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
4674                    crate::v2::TextContent::new("Hello")
4675                )])
4676            ))
4677            .unwrap(),
4678            json!({
4679                "sessionUpdate": "agent_message",
4680                "messageId": "msg_agent_c42b9",
4681                "content": [
4682                    {
4683                        "type": "text",
4684                        "text": "Hello"
4685                    }
4686                ]
4687            })
4688        );
4689
4690        assert_eq!(
4691            serde_json::to_value(SessionUpdate::AgentThought(
4692                AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
4693                    crate::v2::TextContent::new("Need to inspect the call sites first.")
4694                )])
4695            ))
4696            .unwrap(),
4697            json!({
4698                "sessionUpdate": "agent_thought",
4699                "messageId": "msg_thought_a12",
4700                "content": [
4701                    {
4702                        "type": "text",
4703                        "text": "Need to inspect the call sites first."
4704                    }
4705                ]
4706            })
4707        );
4708    }
4709
4710    #[test]
4711    fn test_message_upsert_serialization() {
4712        use serde_json::json;
4713
4714        assert_eq!(
4715            serde_json::to_value(SessionUpdate::UserMessage(
4716                UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
4717            ))
4718            .unwrap(),
4719            json!({
4720                "sessionUpdate": "user_message",
4721                "messageId": "msg_empty",
4722                "content": []
4723            })
4724        );
4725
4726        let empty = serde_json::from_value::<UserMessage>(json!({
4727            "messageId": "msg_empty",
4728            "content": []
4729        }))
4730        .unwrap();
4731        assert!(matches!(
4732            empty.content,
4733            MaybeUndefined::Value(ref content) if content.is_empty()
4734        ));
4735
4736        let patch = serde_json::from_value::<AgentMessage>(json!({
4737            "messageId": "msg_agent_c42b9"
4738        }))
4739        .unwrap();
4740        assert_eq!(patch.content, MaybeUndefined::Undefined);
4741        assert_eq!(patch.meta, MaybeUndefined::Undefined);
4742
4743        let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
4744            "messageId": "msg_agent_c42b9",
4745            "_meta": false
4746        }))
4747        .unwrap();
4748        assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
4749
4750        let patch = serde_json::from_value::<AgentThought>(json!({
4751            "messageId": "msg_thought_a12"
4752        }))
4753        .unwrap();
4754        assert_eq!(patch.content, MaybeUndefined::Undefined);
4755
4756        let clear = serde_json::from_value::<UserMessage>(json!({
4757            "messageId": "msg_user_8f7a1",
4758            "content": null
4759        }))
4760        .unwrap();
4761        assert_eq!(clear.content, MaybeUndefined::Null);
4762
4763        let clear_meta = serde_json::from_value::<UserMessage>(json!({
4764            "messageId": "msg_user_8f7a1",
4765            "_meta": null
4766        }))
4767        .unwrap();
4768        assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4769
4770        let mut meta = Meta::new();
4771        meta.insert("source".to_string(), json!("replay"));
4772
4773        assert_eq!(
4774            serde_json::to_value(SessionUpdate::UserMessage(
4775                UserMessage::new("msg_user_8f7a1").meta(meta)
4776            ))
4777            .unwrap(),
4778            json!({
4779                "sessionUpdate": "user_message",
4780                "messageId": "msg_user_8f7a1",
4781                "_meta": {
4782                    "source": "replay"
4783                }
4784            })
4785        );
4786
4787        assert_eq!(
4788            serde_json::to_value(SessionUpdate::UserMessage(
4789                UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
4790            ))
4791            .unwrap(),
4792            json!({
4793                "sessionUpdate": "user_message",
4794                "messageId": "msg_user_8f7a1",
4795                "_meta": null
4796            })
4797        );
4798    }
4799
4800    #[test]
4801    fn test_usage_update_serialization() {
4802        use serde_json::json;
4803
4804        assert_eq!(
4805            serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
4806                53_000, 200_000
4807            )))
4808            .unwrap(),
4809            json!({
4810                "sessionUpdate": "usage_update",
4811                "used": 53000,
4812                "size": 200_000
4813            })
4814        );
4815
4816        assert_eq!(
4817            serde_json::to_value(SessionUpdate::UsageUpdate(
4818                UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
4819            ))
4820            .unwrap(),
4821            json!({
4822                "sessionUpdate": "usage_update",
4823                "used": 53000,
4824                "size": 200_000,
4825                "cost": {
4826                    "amount": 0.045,
4827                    "currency": "USD"
4828                }
4829            })
4830        );
4831
4832        let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
4833            "sessionUpdate": "usage_update",
4834            "used": 53000,
4835            "size": 200_000,
4836            "cost": null
4837        }))
4838        .unwrap() else {
4839            panic!("expected usage update");
4840        };
4841
4842        assert_eq!(update.cost, None);
4843    }
4844
4845    #[test]
4846    fn error_stop_reason_carries_failure_beside_discriminator() {
4847        use crate::v2::{Error, ErrorStopReason};
4848        use serde_json::json;
4849
4850        let update =
4851            SessionUpdate::StateUpdate(StateUpdate::Idle(IdleStateUpdate::new().stop_reason(
4852                StopReason::from(ErrorStopReason::new().error(Error::auth_required())),
4853            )));
4854        let wire = json!({
4855            "sessionUpdate": "state_update",
4856            "state": "idle",
4857            "stopReason": "error",
4858            "error": { "code": -32000, "message": "Authentication required" }
4859        });
4860        assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4861        assert_eq!(
4862            serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4863            update
4864        );
4865
4866        let with_data =
4867            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::from(
4868                ErrorStopReason::new().error(
4869                    Error::new(-32099, "Provider unavailable").data(json!({ "status": 503 })),
4870                ),
4871            )));
4872        let wire = json!({
4873            "state": "idle",
4874            "stopReason": "error",
4875            "error": {
4876                "code": -32099,
4877                "message": "Provider unavailable",
4878                "data": { "status": 503 }
4879            }
4880        });
4881        assert_eq!(serde_json::to_value(&with_data).unwrap(), wire);
4882        assert_eq!(
4883            serde_json::from_value::<StateUpdate>(wire).unwrap(),
4884            with_data
4885        );
4886
4887        // Missing, null, and malformed details leave a failure without details;
4888        // the stop reason still ends foreground work.
4889        let without_details = StateUpdate::Idle(
4890            IdleStateUpdate::new().stop_reason(StopReason::from(ErrorStopReason::new())),
4891        );
4892        for error in [
4893            None,
4894            Some(json!(null)),
4895            Some(json!("failed")),
4896            Some(json!({ "message": "missing code" })),
4897        ] {
4898            let mut wire = json!({ "state": "idle", "stopReason": "error" });
4899            if let Some(error) = error {
4900                wire["error"] = error;
4901            }
4902            assert_eq!(
4903                serde_json::from_value::<StateUpdate>(wire).unwrap(),
4904                without_details
4905            );
4906        }
4907        assert_eq!(
4908            serde_json::to_value(&without_details).unwrap(),
4909            json!({ "state": "idle", "stopReason": "error" })
4910        );
4911
4912        // `error` is a field of the `error` stop reason only. Beside any other
4913        // stop reason it is an unknown field: the update stays readable and the
4914        // error has nowhere to go.
4915        let parsed: StateUpdate = serde_json::from_value(json!({
4916            "state": "idle",
4917            "stopReason": "end_turn",
4918            "error": { "code": -32603, "message": "Internal error" }
4919        }))
4920        .unwrap();
4921        assert_eq!(
4922            parsed,
4923            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn))
4924        );
4925    }
4926
4927    #[test]
4928    fn flattened_stop_reason_keeps_open_enum_tolerance() {
4929        use crate::v2::OtherStopReason;
4930        use serde_json::json;
4931
4932        let meta: Meta = [("source".into(), json!("test"))].into_iter().collect();
4933
4934        // Omitted, null, and malformed stop reasons all mean no stop reason, as
4935        // when `stopReason` was a separate default-on-error field.
4936        for stop_reason in [None, Some(json!(null)), Some(json!(true)), Some(json!({}))] {
4937            let mut wire = json!({ "state": "idle", "_meta": { "source": "test" } });
4938            if let Some(stop_reason) = stop_reason {
4939                wire["stopReason"] = stop_reason;
4940            }
4941            let StateUpdate::Idle(idle) = serde_json::from_value(wire).unwrap() else {
4942                panic!("expected idle state update");
4943            };
4944            assert_eq!(idle.stop_reason, None);
4945            assert_eq!(idle.meta, Some(meta.clone()));
4946        }
4947
4948        // Unknown stop reasons keep their own fields for proxies and replay,
4949        // while the idle update's fields stay on the idle update.
4950        let wire = json!({
4951            "state": "idle",
4952            "stopReason": "_paused",
4953            "resumeAfter": 30,
4954            "_meta": { "source": "test" }
4955        });
4956        let parsed: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
4957        let expected = StateUpdate::Idle(
4958            IdleStateUpdate::new()
4959                .stop_reason(StopReason::Other(OtherStopReason::new(
4960                    "_paused",
4961                    [("resumeAfter".into(), json!(30))].into_iter().collect(),
4962                )))
4963                .meta(meta),
4964        );
4965        assert_eq!(parsed, expected);
4966        assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
4967    }
4968
4969    #[test]
4970    fn known_stop_reasons_are_never_read_as_other() {
4971        use crate::v2::{ErrorStopReason, OtherStopReason};
4972        use serde_json::json;
4973
4974        let known = [
4975            StopReason::EndTurn,
4976            StopReason::MaxTokens,
4977            StopReason::MaxTurnRequests,
4978            StopReason::Refusal,
4979            StopReason::Cancelled,
4980            StopReason::Error(ErrorStopReason::new()),
4981        ];
4982        // Adding a variant breaks this match, so `known` stays complete.
4983        for stop_reason in &known {
4984            match stop_reason {
4985                StopReason::EndTurn
4986                | StopReason::MaxTokens
4987                | StopReason::MaxTurnRequests
4988                | StopReason::Refusal
4989                | StopReason::Cancelled
4990                | StopReason::Error(_)
4991                | StopReason::Other(_) => {}
4992            }
4993        }
4994
4995        for stop_reason in known {
4996            let update = StateUpdate::Idle(IdleStateUpdate::new().stop_reason(stop_reason));
4997            let wire = serde_json::to_value(&update).unwrap();
4998            assert_eq!(
4999                serde_json::from_value::<StateUpdate>(wire.clone()).unwrap(),
5000                update
5001            );
5002            assert!(
5003                serde_json::from_value::<OtherStopReason>(
5004                    json!({ "stopReason": wire["stopReason"] })
5005                )
5006                .is_err(),
5007                "{} must not be accepted as a custom stop reason",
5008                wire["stopReason"]
5009            );
5010        }
5011    }
5012
5013    #[test]
5014    fn unknown_stop_reason_leaves_usage_on_the_idle_update() {
5015        use serde_json::json;
5016
5017        let usage = json!({ "totalTokens": 10, "inputTokens": 6, "outputTokens": 4 });
5018        let wire = json!({ "state": "idle", "stopReason": "_paused", "usage": usage });
5019        let StateUpdate::Idle(idle) = serde_json::from_value(wire.clone()).unwrap() else {
5020            panic!("expected idle state update");
5021        };
5022        let Some(StopReason::Other(other)) = &idle.stop_reason else {
5023            panic!("expected a custom stop reason");
5024        };
5025        assert!(other.fields.is_empty());
5026
5027        #[cfg(feature = "unstable_end_turn_token_usage")]
5028        {
5029            assert!(idle.usage.is_some());
5030            assert_eq!(serde_json::to_value(&idle).unwrap()["usage"], usage);
5031        }
5032        // Without the feature, `usage` is not a field of the idle update, and it
5033        // must not be smuggled through as a field of the stop reason either.
5034        #[cfg(not(feature = "unstable_end_turn_token_usage"))]
5035        assert!(serde_json::to_value(&idle).unwrap().get("usage").is_none());
5036    }
5037
5038    #[test]
5039    fn test_state_update_serialization() {
5040        use serde_json::json;
5041
5042        assert_eq!(
5043            serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
5044                RunningStateUpdate::new()
5045            )))
5046            .unwrap(),
5047            json!({
5048                "sessionUpdate": "state_update",
5049                "state": "running"
5050            })
5051        );
5052
5053        assert_eq!(
5054            serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
5055                IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
5056            )))
5057            .unwrap(),
5058            json!({
5059                "sessionUpdate": "state_update",
5060                "state": "idle",
5061                "stopReason": "end_turn"
5062            })
5063        );
5064
5065        let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
5066            "sessionUpdate": "state_update",
5067            "state": "requires_action"
5068        }))
5069        .unwrap() else {
5070            panic!("expected state update");
5071        };
5072
5073        assert!(matches!(update, StateUpdate::RequiresAction(_)));
5074
5075        let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
5076            "sessionUpdate": "state_update",
5077            "state": "idle",
5078            "stopReason": null
5079        }))
5080        .unwrap() else {
5081            panic!("expected idle state update");
5082        };
5083
5084        assert_eq!(update.stop_reason, None);
5085
5086        let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
5087            serde_json::from_value(json!({
5088                "sessionUpdate": "state_update",
5089                "state": "_paused",
5090                "label": "Paused"
5091            }))
5092            .unwrap()
5093        else {
5094            panic!("expected unknown state update");
5095        };
5096
5097        assert_eq!(update.state, "_paused");
5098        assert_eq!(update.fields["label"], json!("Paused"));
5099    }
5100
5101    #[test]
5102    fn session_update_preserves_unknown_variant() {
5103        use serde_json::json;
5104
5105        let update: SessionUpdate = serde_json::from_value(json!({
5106            "sessionUpdate": "_status_badge",
5107            "label": "Indexing",
5108            "progress": 0.5
5109        }))
5110        .unwrap();
5111
5112        let SessionUpdate::Other(unknown) = update else {
5113            panic!("expected unknown session update");
5114        };
5115
5116        assert_eq!(unknown.session_update, "_status_badge");
5117        assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
5118        assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
5119
5120        assert_eq!(
5121            serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
5122            json!({
5123                "sessionUpdate": "_status_badge",
5124                "label": "Indexing",
5125                "progress": 0.5
5126            })
5127        );
5128    }
5129
5130    #[test]
5131    fn terminal_session_updates_use_known_discriminators() {
5132        use serde_json::json;
5133
5134        assert_eq!(
5135            serde_json::to_value(SessionUpdate::TerminalUpdate(
5136                TerminalUpdate::new("term_1").command("cargo test")
5137            ))
5138            .unwrap(),
5139            json!({
5140                "sessionUpdate": "terminal_update",
5141                "terminalId": "term_1",
5142                "command": "cargo test"
5143            })
5144        );
5145        assert_eq!(
5146            serde_json::to_value(SessionUpdate::TerminalOutputChunk(
5147                TerminalOutputChunk::new("term_1", "dGVzdAo=")
5148            ))
5149            .unwrap(),
5150            json!({
5151                "sessionUpdate": "terminal_output_chunk",
5152                "terminalId": "term_1",
5153                "data": "dGVzdAo="
5154            })
5155        );
5156    }
5157
5158    #[test]
5159    fn session_update_does_not_hide_malformed_known_terminal_variants() {
5160        use serde_json::json;
5161
5162        assert!(
5163            serde_json::from_value::<SessionUpdate>(json!({
5164                "sessionUpdate": "terminal_update"
5165            }))
5166            .is_err()
5167        );
5168        assert!(
5169            serde_json::from_value::<SessionUpdate>(json!({
5170                "sessionUpdate": "terminal_output_chunk",
5171                "terminalId": "term_1"
5172            }))
5173            .is_err()
5174        );
5175    }
5176
5177    #[test]
5178    fn test_plan_update_serialization() {
5179        use serde_json::json;
5180
5181        let plan_update =
5182            SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
5183                "plan-1",
5184                vec![crate::v2::PlanEntry::new(
5185                    "Step 1",
5186                    crate::v2::PlanEntryPriority::High,
5187                    crate::v2::PlanEntryStatus::Pending,
5188                )],
5189            )));
5190
5191        assert_eq!(
5192            serde_json::to_value(plan_update).unwrap(),
5193            json!({
5194                "sessionUpdate": "plan_update",
5195                "plan": {
5196                    "type": "items",
5197                    "planId": "plan-1",
5198                    "entries": [
5199                        {
5200                            "content": "Step 1",
5201                            "priority": "high",
5202                            "status": "pending"
5203                        }
5204                    ]
5205                }
5206            })
5207        );
5208    }
5209
5210    #[cfg(feature = "unstable_plan_operations")]
5211    #[test]
5212    fn test_plan_removed_serialization() {
5213        use serde_json::json;
5214
5215        assert_eq!(
5216            serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
5217            json!({
5218                "sessionUpdate": "plan_removed",
5219                "planId": "plan-1"
5220            })
5221        );
5222    }
5223
5224    #[test]
5225    fn available_command_uses_type_discriminator() {
5226        use serde_json::json;
5227
5228        let command = AvailableCommand::Command(
5229            Command::new("web", "Search the web")
5230                .input(AvailableCommandInput::Text(TextCommandInput::new("Query"))),
5231        );
5232
5233        let json = serde_json::to_value(&command).unwrap();
5234        assert_eq!(
5235            json,
5236            json!({
5237                "type": "command",
5238                "name": "web",
5239                "description": "Search the web",
5240                "input": { "type": "text", "hint": "Query" }
5241            })
5242        );
5243
5244        let roundtripped: AvailableCommand = serde_json::from_value(json).unwrap();
5245        assert_eq!(roundtripped, command);
5246        assert_eq!(roundtripped.name(), "web");
5247        assert_eq!(roundtripped.description(), "Search the web");
5248    }
5249
5250    #[test]
5251    fn available_command_preserves_unknown_typed_variant() {
5252        use serde_json::json;
5253
5254        let payload = json!({
5255            "type": "_skill",
5256            "name": "pdf-processing",
5257            "description": "Extract text from PDFs",
5258            "uri": "skill://pdf-processing/SKILL.md",
5259            "_meta": { "example.com/source": "mcp" }
5260        });
5261        let command: AvailableCommand = serde_json::from_value(payload.clone()).unwrap();
5262
5263        assert_eq!(command.name(), "pdf-processing");
5264        assert_eq!(command.description(), "Extract text from PDFs");
5265        assert!(command.meta().is_some());
5266
5267        let AvailableCommand::Other(unknown) = &command else {
5268            panic!("expected unknown command");
5269        };
5270        assert_eq!(unknown.type_, "_skill");
5271        assert_eq!(
5272            unknown.fields.get("uri"),
5273            Some(&json!("skill://pdf-processing/SKILL.md"))
5274        );
5275        assert_eq!(serde_json::to_value(&command).unwrap(), payload);
5276    }
5277
5278    #[test]
5279    fn available_command_unknown_tolerates_invalid_meta() {
5280        use serde_json::json;
5281
5282        let command: AvailableCommand = serde_json::from_value(json!({
5283            "type": "_skill",
5284            "name": "pdf-processing",
5285            "description": "Extract text from PDFs",
5286            "_meta": "oops"
5287        }))
5288        .unwrap();
5289
5290        assert!(matches!(command, AvailableCommand::Other(_)));
5291        assert!(command.meta().is_none());
5292    }
5293
5294    #[test]
5295    fn available_command_unknown_does_not_hide_malformed_command_variant() {
5296        use serde_json::json;
5297
5298        // A missing discriminator is rejected rather than defaulting to `command`.
5299        assert!(
5300            serde_json::from_value::<AvailableCommand>(json!({
5301                "name": "test",
5302                "description": "Run project tests"
5303            }))
5304            .is_err()
5305        );
5306        assert!(
5307            serde_json::from_value::<AvailableCommand>(json!({
5308                "type": "command",
5309                "name": "test"
5310            }))
5311            .is_err()
5312        );
5313        assert!(
5314            serde_json::from_value::<OtherAvailableCommand>(json!({
5315                "type": "command",
5316                "name": "test",
5317                "description": "Run project tests"
5318            }))
5319            .is_err()
5320        );
5321        assert!(
5322            serde_json::from_value::<AvailableCommand>(json!({
5323                "type": "_skill",
5324                "name": "pdf-processing"
5325            }))
5326            .is_err()
5327        );
5328    }
5329
5330    #[test]
5331    fn available_command_input_preserves_unknown_typed_variant() {
5332        use serde_json::json;
5333
5334        let input: AvailableCommandInput = serde_json::from_value(json!({
5335            "type": "_choices",
5336            "hint": "Pick one",
5337            "options": ["fast", "careful"]
5338        }))
5339        .unwrap();
5340
5341        let AvailableCommandInput::Other(unknown) = input else {
5342            panic!("expected unknown command input");
5343        };
5344
5345        assert_eq!(unknown.type_, "_choices");
5346        assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
5347        assert_eq!(
5348            unknown.fields.get("options"),
5349            Some(&json!(["fast", "careful"]))
5350        );
5351        assert_eq!(
5352            serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
5353            json!({
5354                "type": "_choices",
5355                "hint": "Pick one",
5356                "options": ["fast", "careful"]
5357            })
5358        );
5359    }
5360
5361    #[test]
5362    fn available_command_input_text_uses_type_discriminator() {
5363        use serde_json::json;
5364
5365        let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
5366
5367        let json = serde_json::to_value(&input).unwrap();
5368        assert_eq!(
5369            json,
5370            json!({
5371                "type": "text",
5372                "hint": "Describe changes"
5373            })
5374        );
5375
5376        let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
5377        assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
5378    }
5379
5380    #[test]
5381    fn request_permission_subject_tool_call_uses_type_discriminator() {
5382        use serde_json::json;
5383
5384        let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
5385
5386        let json = serde_json::to_value(&subject).unwrap();
5387        assert_eq!(
5388            json,
5389            json!({
5390                "type": "tool_call",
5391                "toolCall": {
5392                    "toolCallId": "call_001"
5393                }
5394            })
5395        );
5396
5397        let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5398        assert!(matches!(
5399            roundtripped,
5400            RequestPermissionSubject::ToolCall(_)
5401        ));
5402    }
5403
5404    #[test]
5405    fn request_permission_subject_command_uses_type_discriminator() {
5406        use serde_json::json;
5407
5408        let mut meta = Meta::new();
5409        meta.insert("source".to_string(), json!("shell"));
5410        let subject = RequestPermissionSubject::from(
5411            CommandPermissionSubject::new("cargo test", "/workspace/project")
5412                .tool_call_id("call_001")
5413                .terminal_id("term_1")
5414                .meta(meta),
5415        );
5416
5417        let json = serde_json::to_value(&subject).unwrap();
5418        assert_eq!(
5419            json,
5420            json!({
5421                "type": "command",
5422                "command": "cargo test",
5423                "cwd": "/workspace/project",
5424                "toolCallId": "call_001",
5425                "terminalId": "term_1",
5426                "_meta": {
5427                    "source": "shell"
5428                }
5429            })
5430        );
5431
5432        let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5433        assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
5434    }
5435
5436    #[test]
5437    fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
5438        use serde_json::json;
5439
5440        let subject: RequestPermissionSubject = serde_json::from_value(json!({
5441            "type": "command",
5442            "command": "cargo test",
5443            "cwd": "/workspace/project",
5444            "toolCallId": null,
5445            "terminalId": null,
5446            "_meta": null
5447        }))
5448        .unwrap();
5449
5450        let RequestPermissionSubject::Command(subject) = subject else {
5451            panic!("expected command permission subject");
5452        };
5453        assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
5454        assert_eq!(subject.tool_call_id, None);
5455        assert_eq!(subject.terminal_id, None);
5456        assert_eq!(subject.meta, None);
5457        assert_eq!(
5458            serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
5459            json!({
5460                "type": "command",
5461                "command": "cargo test",
5462                "cwd": "/workspace/project"
5463            })
5464        );
5465    }
5466
5467    #[test]
5468    fn request_permission_subject_preserves_unknown_variant() {
5469        use serde_json::json;
5470
5471        let subject: RequestPermissionSubject = serde_json::from_value(json!({
5472            "type": "_review",
5473            "reason": "needs-review",
5474            "retryAfterSeconds": 30
5475        }))
5476        .unwrap();
5477
5478        let RequestPermissionSubject::Other(unknown) = subject else {
5479            panic!("expected unknown permission subject");
5480        };
5481
5482        assert_eq!(unknown.type_, "_review");
5483        assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
5484        assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
5485        assert_eq!(
5486            serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
5487            json!({
5488                "type": "_review",
5489                "reason": "needs-review",
5490                "retryAfterSeconds": 30
5491            })
5492        );
5493    }
5494
5495    #[test]
5496    fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
5497        use serde_json::json;
5498
5499        assert!(
5500            serde_json::from_value::<RequestPermissionSubject>(json!({
5501                "type": "tool_call"
5502            }))
5503            .is_err()
5504        );
5505        assert!(
5506            serde_json::from_value::<RequestPermissionSubject>(json!({
5507                "type": 1
5508            }))
5509            .is_err()
5510        );
5511        assert!(
5512            serde_json::from_value::<RequestPermissionSubject>(json!({
5513                "type": "command",
5514                "cwd": "/workspace/project"
5515            }))
5516            .is_err()
5517        );
5518        assert!(
5519            serde_json::from_value::<RequestPermissionSubject>(json!({
5520                "type": "command",
5521                "command": "cargo test"
5522            }))
5523            .is_err()
5524        );
5525        assert!(
5526            serde_json::from_value::<RequestPermissionSubject>(json!({
5527                "type": "command",
5528                "command": "cargo test",
5529                "cwd": null
5530            }))
5531            .is_err()
5532        );
5533    }
5534
5535    #[test]
5536    fn request_permission_title_and_description_are_separate_from_tool_call_content() {
5537        use serde_json::json;
5538
5539        let request =
5540            RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
5541                .description("Allow this tool to edit src/main.rs?")
5542                .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
5543                    "call_001",
5544                )));
5545
5546        assert_eq!(
5547            serde_json::to_value(request).unwrap(),
5548            json!({
5549                "sessionId": "sess_abc123def456",
5550                "title": "Approve file edit?",
5551                "description": "Allow this tool to edit src/main.rs?",
5552                "subject": {
5553                    "type": "tool_call",
5554                    "toolCall": {
5555                        "toolCallId": "call_001"
5556                    }
5557                },
5558                "options": []
5559            })
5560        );
5561    }
5562
5563    #[test]
5564    fn request_permission_requires_title_and_allows_missing_subject() {
5565        use serde_json::json;
5566
5567        let request = RequestPermissionRequest::new(
5568            "sess_abc123def456",
5569            "Approve elevated permissions?",
5570            Vec::new(),
5571        );
5572
5573        assert_eq!(
5574            serde_json::to_value(request).unwrap(),
5575            json!({
5576                "sessionId": "sess_abc123def456",
5577                "title": "Approve elevated permissions?",
5578                "options": []
5579            })
5580        );
5581
5582        let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
5583            "sessionId": "sess_abc123def456",
5584            "title": "Approve elevated permissions?",
5585            "options": []
5586        }))
5587        .unwrap();
5588        assert!(missing_subject.subject.is_none());
5589
5590        let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
5591            "sessionId": "sess_abc123def456",
5592            "title": "Approve elevated permissions?",
5593            "subject": null,
5594            "options": []
5595        }))
5596        .unwrap();
5597        assert!(null_subject.subject.is_none());
5598
5599        assert!(
5600            serde_json::from_value::<RequestPermissionRequest>(json!({
5601                "sessionId": "sess_abc123def456",
5602                "options": []
5603            }))
5604            .is_err()
5605        );
5606    }
5607
5608    #[test]
5609    fn request_permission_outcome_preserves_unknown_variant() {
5610        use serde_json::json;
5611
5612        let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
5613            "outcome": "_defer",
5614            "reason": "needs-review",
5615            "retryAfterSeconds": 30
5616        }))
5617        .unwrap();
5618
5619        let RequestPermissionOutcome::Other(unknown) = outcome else {
5620            panic!("expected unknown permission outcome");
5621        };
5622
5623        assert_eq!(unknown.outcome, "_defer");
5624        assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
5625        assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
5626        assert_eq!(
5627            serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
5628            json!({
5629                "outcome": "_defer",
5630                "reason": "needs-review",
5631                "retryAfterSeconds": 30
5632            })
5633        );
5634    }
5635
5636    #[test]
5637    fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
5638        use serde_json::json;
5639
5640        assert!(
5641            serde_json::from_value::<RequestPermissionOutcome>(json!({
5642                "outcome": "selected"
5643            }))
5644            .is_err()
5645        );
5646        assert!(
5647            serde_json::from_value::<RequestPermissionOutcome>(json!({
5648                "outcome": 1
5649            }))
5650            .is_err()
5651        );
5652    }
5653
5654    #[test]
5655    fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
5656        use serde_json::json;
5657
5658        assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
5659        assert!(
5660            serde_json::from_value::<AvailableCommandInput>(json!({
5661                "hint": "Pick one"
5662            }))
5663            .is_err()
5664        );
5665        assert!(
5666            serde_json::from_value::<AvailableCommandInput>(json!({
5667                "type": 1,
5668                "hint": "Pick one"
5669            }))
5670            .is_err()
5671        );
5672        assert!(
5673            serde_json::from_value::<OtherAvailableCommandInput>(json!({
5674                "type": "text",
5675                "hint": "Pick one"
5676            }))
5677            .is_err()
5678        );
5679    }
5680
5681    #[cfg(feature = "unstable_mcp_over_acp")]
5682    #[test]
5683    fn test_agent_mcp_request_method_names() {
5684        use serde_json::json;
5685
5686        let params: serde_json::Map<String, serde_json::Value> =
5687            [("cursor".to_string(), json!("abc"))].into_iter().collect();
5688
5689        assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
5690        assert_eq!(
5691            AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
5692                "server-1",
5693                "req-1",
5694                "tools/list"
5695            )))
5696            .method(),
5697            "mcp/message"
5698        );
5699        assert_eq!(
5700            serde_json::to_value(
5701                MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
5702            )
5703            .unwrap(),
5704            json!({
5705                "serverId": "server-1",
5706                "requestId": "req-1",
5707                "method": "tools/list",
5708                "params": { "cursor": "abc" }
5709            })
5710        );
5711
5712        let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
5713            "serverId": "server-1",
5714            "requestId": "req-1",
5715            "method": "tools/list",
5716            "params": null,
5717            "_meta": null
5718        }))
5719        .unwrap();
5720        assert_eq!(request_with_null_params.params, None);
5721        assert_eq!(request_with_null_params.meta, None);
5722        for key in ["serverId", "requestId", "method"] {
5723            let mut value =
5724                json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5725            value.as_object_mut().unwrap().remove(key);
5726            assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5727        }
5728        for key in ["serverId", "requestId", "method"] {
5729            let mut value =
5730                json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5731            value[key] = serde_json::Value::Null;
5732            assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5733        }
5734    }
5735
5736    #[test]
5737    fn test_auth_capabilities_serialize_terminal_support_as_object() {
5738        use serde_json::json;
5739
5740        let capabilities = ClientAuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
5741
5742        assert_eq!(
5743            serde_json::to_value(&capabilities).unwrap(),
5744            json!({
5745                "terminal": {}
5746            })
5747        );
5748
5749        let deserialized: ClientAuthCapabilities = serde_json::from_value(json!({
5750            "terminal": false
5751        }))
5752        .unwrap();
5753        assert!(deserialized.terminal.is_none());
5754    }
5755
5756    #[test]
5757    fn request_permission_request_rejects_malformed_options() {
5758        use serde_json::json;
5759
5760        assert!(
5761            serde_json::from_value::<RequestPermissionRequest>(json!({
5762                "sessionId": "sess-1",
5763                "title": "Run tool?",
5764                "options": "not-an-array"
5765            }))
5766            .is_err()
5767        );
5768        assert!(
5769            serde_json::from_value::<RequestPermissionRequest>(json!({
5770                "sessionId": "sess-1",
5771                "title": "Run tool?",
5772                "options": [{"optionId": "allow"}]
5773            }))
5774            .is_err()
5775        );
5776    }
5777
5778    #[cfg(feature = "unstable_plan_operations")]
5779    #[test]
5780    fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
5781        use serde_json::json;
5782
5783        assert!(
5784            serde_json::from_value::<SessionUpdate>(json!({
5785                "sessionUpdate": "plan_removed"
5786            }))
5787            .is_err()
5788        );
5789    }
5790}