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