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