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    /// When a client sends a `session/cancel` notification, it MUST respond to all
3534    /// pending `session/request_permission` requests in that session with this
3535    /// `cancelled` outcome, without answering any request twice. Requests in other
3536    /// sessions are unaffected.
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    /// If the client cancels active session work via `session/cancel`, it MUST
3981    /// respond to this request with the `cancelled` outcome.
3982    ///
3983    /// See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/tool-calls#requesting-permission)
3984    RequestPermissionRequest(Box<RequestPermissionRequest>),
3985    /// Requests structured user input via a form or URL.
3986    ///
3987    /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/v2/elicitation)
3988    CreateElicitationRequest(Box<CreateElicitationRequest>),
3989    /// **UNSTABLE**
3990    ///
3991    /// This capability is not part of the spec yet, and may be removed or changed at any point.
3992    ///
3993    /// Exchanges an MCP-over-ACP message.
3994    #[cfg(feature = "unstable_mcp_over_acp")]
3995    MessageMcpRequest(Box<MessageMcpRequest>),
3996    /// Handles extension method requests from the agent.
3997    ///
3998    /// Allows the Agent to send an arbitrary request that is not part of the ACP spec.
3999    /// Extension methods provide a way to add custom functionality while maintaining
4000    /// protocol compatibility.
4001    ///
4002    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
4003    ExtMethodRequest(Box<ExtRequest>),
4004}
4005
4006impl AgentRequest {
4007    /// Returns the corresponding method name of the request.
4008    #[must_use]
4009    pub fn method(&self) -> &str {
4010        match self {
4011            Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
4012            Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
4013            #[cfg(feature = "unstable_mcp_over_acp")]
4014            Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
4015            Self::ExtMethodRequest(ext_request) => &ext_request.method,
4016        }
4017    }
4018}
4019
4020/// All possible responses that a client can send to an agent.
4021///
4022/// This enum is used internally for routing RPC responses. You typically won't need
4023/// to use this directly - the responses are handled automatically by the connection.
4024///
4025/// These are responses to the corresponding `AgentRequest` variants.
4026#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
4027#[derive(Clone, Debug, Serialize, Deserialize)]
4028#[serde(untagged)]
4029#[cfg_attr(feature = "schemars", schemars(inline))]
4030#[non_exhaustive]
4031pub enum ClientResponse {
4032    /// Successful result returned for a `session/request_permission` request.
4033    RequestPermissionResponse(Box<RequestPermissionResponse>),
4034    /// Successful result returned for a `elicitation/create` request.
4035    CreateElicitationResponse(Box<CreateElicitationResponse>),
4036    /// Successful result returned by an MCP-over-ACP `mcp/message` request.
4037    #[cfg(feature = "unstable_mcp_over_acp")]
4038    MessageMcpResponse(Box<MessageMcpResponse>),
4039    /// Successful result returned by an extension method outside the core ACP method set.
4040    ExtMethodResponse(Box<ExtResponse>),
4041}
4042
4043/// All possible notifications that an agent can send to a client.
4044///
4045/// This enum is used internally for routing RPC notifications. You typically won't need
4046/// to use this directly.
4047///
4048/// Notifications do not expect a response.
4049#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
4050#[derive(Clone, Debug, Serialize, Deserialize)]
4051#[serde(untagged)]
4052#[cfg_attr(feature = "schemars", schemars(inline))]
4053#[non_exhaustive]
4054pub enum AgentNotification {
4055    /// Handles session update notifications from the agent.
4056    ///
4057    /// This is a notification endpoint (no response expected) that receives
4058    /// updates about session activity, including message updates, message chunks,
4059    /// tool calls, and execution plans.
4060    ///
4061    /// Note: Clients SHOULD continue accepting tool call updates even after
4062    /// sending a `session/cancel` notification, as the agent may send final
4063    /// updates before reporting an idle `state_update` with the cancelled
4064    /// stop reason.
4065    ///
4066    /// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#3-agent-reports-output)
4067    UpdateSessionNotification(Box<UpdateSessionNotification>),
4068    /// Notification that a URL-based elicitation has completed.
4069    ///
4070    /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/v2/elicitation#url-completion)
4071    CompleteElicitationNotification(Box<CompleteElicitationNotification>),
4072    /// **UNSTABLE**
4073    ///
4074    /// This capability is not part of the spec yet, and may be removed or changed at any point.
4075    ///
4076    /// Notification that the agent's authentication state changed.
4077    ///
4078    /// Only sent by agents that advertise the `auth.status` capability.
4079    #[cfg(feature = "unstable_auth_status")]
4080    AuthStatusUpdateNotification(Box<AuthStatusUpdateNotification>),
4081    /// Handles extension notifications from the agent.
4082    ///
4083    /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec.
4084    /// Extension notifications provide a way to send one-way messages for custom functionality
4085    /// while maintaining protocol compatibility.
4086    ///
4087    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
4088    ExtNotification(Box<ExtNotification>),
4089}
4090
4091impl AgentNotification {
4092    /// Returns the corresponding method name of the notification.
4093    #[must_use]
4094    pub fn method(&self) -> &str {
4095        match self {
4096            Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
4097            Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
4098            #[cfg(feature = "unstable_auth_status")]
4099            Self::AuthStatusUpdateNotification(_) => CLIENT_METHOD_NAMES.auth_status_update,
4100            Self::ExtNotification(ext_notification) => &ext_notification.method,
4101        }
4102    }
4103}
4104
4105#[cfg(test)]
4106mod tests {
4107
4108    #[test]
4109    fn background_list_round_trips_on_state_updates() {
4110        use serde_json::json;
4111        let idle = super::StateUpdate::Idle(super::IdleStateUpdate::new().background(vec![
4112            super::BackgroundWork::Terminal(super::BackgroundTerminal::new("term_003")),
4113            super::BackgroundWork::ToolCall(super::BackgroundToolCall::new("call_012")),
4114        ]));
4115        let wire = json!({
4116            "state": "idle",
4117            "background": [
4118                { "type": "terminal", "terminalId": "term_003" },
4119                { "type": "tool_call", "toolCallId": "call_012" }
4120            ]
4121        });
4122        assert_eq!(serde_json::to_value(&idle).unwrap(), wire);
4123        assert_eq!(
4124            serde_json::from_value::<super::StateUpdate>(wire).unwrap(),
4125            idle
4126        );
4127
4128        assert_eq!(idle.background().map(<[_]>::len), Some(2));
4129
4130        let empty = super::RunningStateUpdate::new().background(vec![]);
4131        assert_eq!(
4132            serde_json::to_value(&empty).unwrap(),
4133            json!({ "background": [] })
4134        );
4135        assert_eq!(
4136            super::StateUpdate::Running(empty).background(),
4137            Some(&[][..])
4138        );
4139        assert_eq!(
4140            super::StateUpdate::RequiresAction(super::RequiresActionStateUpdate::new())
4141                .background(),
4142            None
4143        );
4144        let custom: super::StateUpdate = serde_json::from_value(json!({
4145            "state": "_waiting",
4146            "background": [{ "type": "terminal", "terminalId": "term_003" }]
4147        }))
4148        .unwrap();
4149        assert_eq!(custom.background(), None);
4150    }
4151
4152    #[test]
4153    fn background_list_keeps_unknown_items_and_drops_malformed_lists() {
4154        use serde_json::json;
4155        let parsed: super::RunningStateUpdate = serde_json::from_value(json!({
4156            "background": [{ "type": "_cron", "schedule": "* * * * *" }]
4157        }))
4158        .unwrap();
4159        let Some([super::BackgroundWork::Other(other)]) = parsed.background.as_deref() else {
4160            panic!("expected one unknown item: {parsed:?}");
4161        };
4162        assert_eq!(other.kind, "_cron");
4163        assert_eq!(other.fields["schedule"], json!("* * * * *"));
4164
4165        for background in [
4166            json!(null),
4167            json!("oops"),
4168            json!([{ "type": "terminal" }]),
4169            json!([{ "type": "tool_call", "toolCallId": "call_1" }, { "type": "terminal", "terminalId": 7 }]),
4170        ] {
4171            let parsed: super::IdleStateUpdate =
4172                serde_json::from_value(json!({ "background": background })).unwrap();
4173            assert_eq!(parsed.background, None, "{background}");
4174        }
4175    }
4176
4177    #[test]
4178    fn replacement_lists_reject_non_arrays_instead_of_clearing() {
4179        use serde_json::json;
4180        for (update, key) in [
4181            ("config_option_update", "configOptions"),
4182            ("available_commands_update", "availableCommands"),
4183        ] {
4184            let empty = json!({ "sessionUpdate": update, key: [] });
4185            serde_json::from_value::<super::SessionUpdate>(empty).unwrap();
4186            for bad in [json!("oops"), json!(null), json!({})] {
4187                let wire = json!({ "sessionUpdate": update, key: bad });
4188                assert!(
4189                    serde_json::from_value::<super::SessionUpdate>(wire).is_err(),
4190                    "{update} {bad}"
4191                );
4192            }
4193        }
4194        let plan = |entries: serde_json::Value| {
4195            json!({ "sessionUpdate": "plan_update",
4196                "plan": { "type": "items", "planId": "p", "entries": entries } })
4197        };
4198        serde_json::from_value::<super::SessionUpdate>(plan(json!([]))).unwrap();
4199        assert!(serde_json::from_value::<super::SessionUpdate>(plan(json!("oops"))).is_err());
4200        // Invalid items are still skipped.
4201        let skipped =
4202            plan(json!([42, { "content": "a", "priority": "high", "status": "pending" }]));
4203        let super::SessionUpdate::PlanUpdate(update) =
4204            serde_json::from_value::<super::SessionUpdate>(skipped).unwrap()
4205        else {
4206            panic!("expected plan_update");
4207        };
4208        let crate::v2::PlanUpdateContent::Items(items) = update.plan else {
4209            panic!("expected items");
4210        };
4211        assert_eq!(items.entries.len(), 1);
4212    }
4213
4214    #[test]
4215    fn other_available_command_preserves_meta_verbatim() {
4216        for meta in [serde_json::json!({ "k": 1 }), serde_json::json!("bad")] {
4217            let wire = serde_json::json!({ "type": "_custom", "name": "c",
4218                "description": "d", "_meta": meta, "extra": true });
4219            let command: super::AvailableCommand = serde_json::from_value(wire.clone()).unwrap();
4220            assert_eq!(command.meta(), meta.as_object());
4221            assert_eq!(serde_json::to_value(&command).unwrap(), wire);
4222        }
4223    }
4224
4225    #[test]
4226    fn cancelled_permission_outcome_wire_format() {
4227        let outcome: RequestPermissionOutcome =
4228            serde_json::from_value(serde_json::json!({ "outcome": "cancelled" })).unwrap();
4229        assert_eq!(outcome, CancelledPermissionOutcome::new().into());
4230        assert_eq!(
4231            serde_json::to_value(&outcome).unwrap(),
4232            serde_json::json!({ "outcome": "cancelled" })
4233        );
4234
4235        let with_meta: RequestPermissionOutcome = CancelledPermissionOutcome::new()
4236            .meta(serde_json::Map::from_iter([(
4237                "k".to_string(),
4238                serde_json::json!(1),
4239            )]))
4240            .into();
4241        let json = serde_json::to_value(&with_meta).unwrap();
4242        assert_eq!(
4243            json,
4244            serde_json::json!({ "outcome": "cancelled", "_meta": { "k": 1 } })
4245        );
4246        assert_eq!(
4247            serde_json::from_value::<RequestPermissionOutcome>(json).unwrap(),
4248            with_meta
4249        );
4250    }
4251
4252    use super::*;
4253
4254    #[cfg(feature = "unstable_subagents")]
4255    #[test]
4256    fn subagent_update_serializes_as_upsert() {
4257        use serde_json::json;
4258
4259        let announced =
4260            SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
4261                SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
4262            ));
4263        let wire = json!({
4264            "sessionUpdate": "subagent_update",
4265            "sessionId": "sess_child_1",
4266            "capabilities": { "cancel": {} }
4267        });
4268        assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
4269        assert_eq!(
4270            serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4271            announced
4272        );
4273
4274        let minimal = SubagentUpdate::new("sess_child_1");
4275        assert_eq!(
4276            serde_json::to_value(&minimal).unwrap(),
4277            json!({
4278                "sessionId": "sess_child_1"
4279            })
4280        );
4281        assert!(minimal.capabilities.is_undefined());
4282        assert!(minimal.state.is_undefined());
4283        assert!(minimal.meta.is_undefined());
4284
4285        // Patch semantics distinguish omitted fields from explicit nulls.
4286        let cleared_wire = json!({
4287            "sessionId": "sess_child_1",
4288            "capabilities": null,
4289            "state": null,
4290            "_meta": null
4291        });
4292        let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4293        assert!(cleared.capabilities.is_null());
4294        assert!(cleared.state.is_null());
4295        assert!(cleared.meta.is_null());
4296        assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4297
4298        let revoked: SubagentUpdate = serde_json::from_value(json!({
4299            "sessionId": "sess_child_1",
4300            "capabilities": {}
4301        }))
4302        .unwrap();
4303        assert_eq!(
4304            revoked.capabilities,
4305            MaybeUndefined::Value(SubagentSessionCapabilities::new())
4306        );
4307        assert!(
4308            matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
4309        );
4310
4311        assert!(
4312            serde_json::from_value::<SessionUpdate>(json!({
4313                "sessionUpdate": "subagent_update"
4314            }))
4315            .is_err()
4316        );
4317    }
4318
4319    #[cfg(feature = "unstable_subagents")]
4320    #[test]
4321    fn subagent_cancel_capability_is_optional_object() {
4322        use serde_json::json;
4323
4324        for wire in [
4325            json!({}),
4326            json!({"cancel": null}),
4327            json!({"cancel": true}),
4328            json!({"cancel": false}),
4329        ] {
4330            let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
4331            assert!(child.cancel.is_none());
4332            assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
4333        }
4334        let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
4335        assert_eq!(
4336            serde_json::to_value(&enabled).unwrap(),
4337            json!({"cancel": {}})
4338        );
4339        assert_eq!(
4340            serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
4341            enabled
4342        );
4343        let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
4344        let with_meta =
4345            SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
4346        let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
4347        assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
4348        assert_eq!(
4349            serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
4350            with_meta
4351        );
4352        assert!(enabled.cancel.is_some());
4353        assert!(enabled.cancel(None).cancel.is_none());
4354        let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
4355        assert_eq!(
4356            serde_json::to_value(removed).unwrap(),
4357            json!({"sessionId": "child", "capabilities": {}})
4358        );
4359    }
4360
4361    #[cfg(feature = "unstable_subagents")]
4362    #[test]
4363    fn subagent_display_metadata_uses_patch_semantics() {
4364        use serde_json::json;
4365
4366        let update = SubagentUpdate::new("child")
4367            .title("Test investigator".to_string())
4368            .description("Investigates platform-specific test failures.".to_string());
4369        let wire = json!({
4370            "sessionId": "child",
4371            "title": "Test investigator",
4372            "description": "Investigates platform-specific test failures."
4373        });
4374        assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4375        assert_eq!(
4376            serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4377            update
4378        );
4379
4380        let minimal = SubagentUpdate::new("child");
4381        assert!(minimal.title.is_undefined());
4382        assert!(minimal.description.is_undefined());
4383        assert_eq!(
4384            serde_json::to_value(&minimal).unwrap(),
4385            json!({"sessionId": "child"})
4386        );
4387        let cleared_wire = json!({
4388            "sessionId": "child", "title": null, "description": null
4389        });
4390        let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4391        assert!(cleared.title.is_null());
4392        assert!(cleared.description.is_null());
4393        assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4394
4395        let title_only: SubagentUpdate = serde_json::from_value(json!({
4396            "sessionId": "child", "title": "Updated title"
4397        }))
4398        .unwrap();
4399        assert_eq!(
4400            title_only.title,
4401            MaybeUndefined::Value("Updated title".to_string())
4402        );
4403        assert!(title_only.description.is_undefined());
4404        let malformed: SubagentUpdate = serde_json::from_value(json!({
4405            "sessionId": "child", "title": false, "description": false
4406        }))
4407        .unwrap();
4408        assert_eq!(malformed, minimal);
4409    }
4410
4411    #[cfg(feature = "unstable_subagents")]
4412    #[test]
4413    fn subagent_directories_use_strict_patch_semantics() {
4414        use serde_json::json;
4415
4416        let update = SubagentUpdate::new("child")
4417            .cwd("/home/user/project/.worktrees/investigator")
4418            .additional_directories(vec![]);
4419        let wire = json!({
4420            "sessionId": "child",
4421            "cwd": "/home/user/project/.worktrees/investigator",
4422            "additionalDirectories": []
4423        });
4424        assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4425        assert_eq!(
4426            serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4427            update
4428        );
4429
4430        let inherited: SubagentUpdate =
4431            serde_json::from_value(json!({"sessionId": "child"})).unwrap();
4432        assert!(inherited.cwd.is_undefined());
4433        assert!(inherited.additional_directories.is_undefined());
4434
4435        let cleared_wire = json!({
4436            "sessionId": "child", "cwd": null, "additionalDirectories": null
4437        });
4438        let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4439        assert!(cleared.cwd.is_null());
4440        assert!(cleared.additional_directories.is_null());
4441        assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4442
4443        for malformed in [
4444            json!({"sessionId": "child", "cwd": false}),
4445            json!({"sessionId": "child", "additionalDirectories": "/tmp/wt"}),
4446            json!({"sessionId": "child", "additionalDirectories": ["/tmp/wt", false]}),
4447        ] {
4448            assert!(
4449                serde_json::from_value::<SubagentUpdate>(malformed.clone()).is_err(),
4450                "{malformed}"
4451            );
4452        }
4453    }
4454
4455    #[cfg(feature = "unstable_subagents")]
4456    #[test]
4457    fn subagent_notification_keeps_parent_and_child_ids_nested() {
4458        use serde_json::json;
4459
4460        let notification = UpdateSessionNotification::new(
4461            "parent",
4462            SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
4463        );
4464        let wire = json!({
4465            "sessionId": "parent",
4466            "update": {
4467                "sessionUpdate": "subagent_update",
4468                "sessionId": "child"
4469            }
4470        });
4471        assert_eq!(serde_json::to_value(&notification).unwrap(), wire);
4472        assert_eq!(
4473            serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
4474            notification
4475        );
4476        for malformed in [
4477            json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
4478            json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
4479        ] {
4480            assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
4481        }
4482    }
4483
4484    #[cfg(feature = "unstable_subagents")]
4485    #[test]
4486    fn subagent_mirrors_normal_child_state_notifications() {
4487        use serde_json::json;
4488
4489        let association = SubagentUpdate::new("sess_child");
4490        for state in [
4491            StateUpdate::Running(RunningStateUpdate::new()),
4492            StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
4493            StateUpdate::Unknown(UnknownStateUpdate::new()),
4494            StateUpdate::Running(RunningStateUpdate::new()),
4495            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
4496            StateUpdate::Running(RunningStateUpdate::new()),
4497            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
4498        ] {
4499            let child_notification = UpdateSessionNotification::new(
4500                association.session_id.clone(),
4501                SessionUpdate::StateUpdate(state.clone()),
4502            );
4503            let parent_notification = UpdateSessionNotification::new(
4504                "sess_parent",
4505                SessionUpdate::SubagentUpdate(association.clone().state(state)),
4506            );
4507            let child_wire = serde_json::to_value(&child_notification).unwrap();
4508            let parent_wire = serde_json::to_value(&parent_notification).unwrap();
4509            assert_eq!(child_wire["sessionId"], json!("sess_child"));
4510            assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
4511            assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
4512            assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
4513            let mut child_snapshot = child_wire["update"].clone();
4514            child_snapshot
4515                .as_object_mut()
4516                .unwrap()
4517                .remove("sessionUpdate");
4518            assert_eq!(parent_wire["update"]["state"], child_snapshot);
4519            assert_eq!(
4520                serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
4521                child_notification
4522            );
4523            assert_eq!(
4524                serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
4525                parent_notification
4526            );
4527        }
4528    }
4529
4530    #[cfg(feature = "unstable_subagents")]
4531    #[test]
4532    fn subagent_state_uses_whole_snapshot_patch_semantics() {
4533        use serde_json::json;
4534
4535        let minimal = SubagentUpdate::new("child");
4536        assert!(minimal.state.is_undefined());
4537        assert_eq!(
4538            serde_json::to_value(&minimal).unwrap(),
4539            json!({"sessionId": "child"})
4540        );
4541
4542        let cleared = minimal.clone().state(None::<StateUpdate>);
4543        assert!(cleared.state.is_null());
4544        assert_eq!(
4545            serde_json::to_value(&cleared).unwrap(),
4546            json!({"sessionId": "child", "state": null})
4547        );
4548        assert_eq!(
4549            serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
4550                .unwrap(),
4551            cleared
4552        );
4553
4554        for snapshot in [
4555            json!({"state": "running", "_meta": {"source": "worker"}}),
4556            json!({"state": "idle", "stopReason": "end_turn"}),
4557            json!({"state": "requires_action"}),
4558            json!({"state": "unknown"}),
4559            json!({"state": "_custom", "detail": {"phase": 2}}),
4560        ] {
4561            let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
4562            let update = minimal.clone().state(state.clone());
4563            let wire = json!({"sessionId": "child", "state": snapshot});
4564            assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4565            assert_eq!(
4566                serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4567                update
4568            );
4569            assert_eq!(update.state, MaybeUndefined::Value(state));
4570        }
4571
4572        // Each concrete value is the entire replacement snapshot, not a merge
4573        // with a previous state's stop reason or metadata.
4574        let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
4575            IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
4576        ));
4577        let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
4578        assert_eq!(
4579            serde_json::to_value(running).unwrap(),
4580            json!({"sessionId": "child", "state": {"state": "running"}})
4581        );
4582        for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
4583            let parsed: SubagentUpdate =
4584                serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
4585            assert_eq!(parsed, minimal);
4586        }
4587    }
4588
4589    #[cfg(feature = "unstable_subagents")]
4590    #[test]
4591    fn unknown_activity_is_a_known_state_update() {
4592        use serde_json::json;
4593
4594        let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
4595            UnknownStateUpdate::new().meta(
4596                [("source".into(), json!("worker"))]
4597                    .into_iter()
4598                    .collect::<Meta>(),
4599            ),
4600        ));
4601        let wire = json!({
4602            "sessionUpdate": "state_update",
4603            "state": "unknown",
4604            "_meta": { "source": "worker" }
4605        });
4606        assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4607        assert_eq!(
4608            serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4609            update
4610        );
4611
4612        for meta in [json!(null), json!(false)] {
4613            let state: StateUpdate = serde_json::from_value(json!({
4614                "state": "unknown",
4615                "_meta": meta
4616            }))
4617            .unwrap();
4618            assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
4619            assert_eq!(
4620                serde_json::to_value(state).unwrap(),
4621                json!({"state": "unknown"})
4622            );
4623        }
4624        assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
4625    }
4626
4627    #[cfg(not(feature = "unstable_subagents"))]
4628    #[test]
4629    fn unknown_activity_is_preserved_without_subagents_feature() {
4630        let wire = serde_json::json!({
4631            "sessionUpdate": "state_update",
4632            "state": "unknown",
4633            "_meta": { "source": "worker" }
4634        });
4635        let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4636        let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
4637            panic!("expected unrecognized state payload");
4638        };
4639        assert_eq!(state.state, "unknown");
4640        assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4641    }
4642
4643    #[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
4644    #[test]
4645    fn subagent_schema_carries_optional_nullable_state_snapshot() {
4646        use serde_json::json;
4647
4648        let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
4649        let properties = schema["properties"].as_object().unwrap();
4650        assert!(properties.contains_key("sessionId"));
4651        assert!(properties.contains_key("title"));
4652        assert!(properties.contains_key("description"));
4653        assert!(properties.contains_key("capabilities"));
4654        assert!(properties.contains_key("state"));
4655        assert!(properties.contains_key("_meta"));
4656        assert!(!properties.contains_key("name"));
4657        assert!(!properties.contains_key("task"));
4658        assert_eq!(schema["required"], json!(["sessionId"]));
4659        assert_eq!(
4660            properties["state"]["x-deserialize-default-on-error"],
4661            json!(true)
4662        );
4663        let state_variants = properties["state"]["anyOf"].as_array().unwrap();
4664        assert!(state_variants.contains(&json!({"type": "null"})));
4665        assert!(
4666            state_variants
4667                .iter()
4668                .any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
4669        );
4670        let child =
4671            serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
4672        let variants = child["properties"]["cancel"]["anyOf"]
4673            .as_array()
4674            .expect("cancel must allow the capability object or null");
4675        assert!(variants.contains(&json!({"type": "null"})));
4676        assert!(
4677            variants
4678                .iter()
4679                .any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
4680        );
4681        assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
4682        let cancel =
4683            serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
4684        assert_eq!(cancel["type"], "object");
4685        assert!(
4686            !child["required"]
4687                .as_array()
4688                .is_some_and(|required| required.contains(&json!("cancel")))
4689        );
4690    }
4691
4692    #[cfg(not(feature = "unstable_subagents"))]
4693    #[test]
4694    fn unsupported_subagent_update_is_preserved() {
4695        let wire = serde_json::json!({
4696            "sessionUpdate": "subagent_update",
4697            "sessionId": "sess_child",
4698            "capabilities": { "cancel": {} }
4699        });
4700        let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4701        assert!(matches!(&parsed, SessionUpdate::Other(_)));
4702        assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4703    }
4704
4705    #[test]
4706    fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
4707        use serde_json::json;
4708
4709        let mut meta = Meta::new();
4710        meta.insert("source".into(), json!("fallback"));
4711        let v2_notice = SessionUpdate::Notice(
4712            Notice::new(NoticeSeverity::Error, "Provider degraded")
4713                .description("Requests may take longer than usual.")
4714                .meta(meta.clone()),
4715        );
4716        let expected = json!({
4717            "sessionUpdate": "notice",
4718            "severity": "error",
4719            "title": "Provider degraded",
4720            "description": "Requests may take longer than usual.",
4721            "_meta": { "source": "fallback" }
4722        });
4723        assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
4724
4725        let v1_notice = crate::v1::SessionUpdate::Notice(
4726            crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
4727                .description("Requests may take longer than usual.")
4728                .meta(meta),
4729        );
4730        assert_eq!(
4731            serde_json::to_value(v2_notice).unwrap(),
4732            serde_json::to_value(v1_notice).unwrap()
4733        );
4734
4735        let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
4736            "sessionUpdate": "notice",
4737            "severity": "critical",
4738            "title": "Provider degraded",
4739            "description": null,
4740            "_meta": null
4741        }))
4742        .unwrap() else {
4743            panic!("expected notice");
4744        };
4745
4746        assert_eq!(
4747            notice.severity,
4748            NoticeSeverity::Other("critical".to_string())
4749        );
4750        assert_eq!(notice.description, None);
4751        assert_eq!(notice.meta, None);
4752        assert_eq!(
4753            serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
4754            json!({
4755                "sessionUpdate": "notice",
4756                "severity": "critical",
4757                "title": "Provider degraded"
4758            })
4759        );
4760    }
4761
4762    #[test]
4763    fn malformed_known_notice_is_not_hidden_as_unknown() {
4764        use serde_json::json;
4765
4766        for malformed in [
4767            json!({
4768                "sessionUpdate": "notice",
4769                "severity": "warning",
4770                "title": 42
4771            }),
4772            json!({
4773                "sessionUpdate": "notice",
4774                "severity": 42,
4775                "title": "MCP server unavailable"
4776            }),
4777            json!({
4778                "sessionUpdate": "notice",
4779                "severity": "warning"
4780            }),
4781            json!({
4782                "sessionUpdate": "notice",
4783                "severity": "warning",
4784                "title": null
4785            }),
4786            json!({
4787                "sessionUpdate": "notice",
4788                "title": "MCP server unavailable"
4789            }),
4790            json!({
4791                "sessionUpdate": "notice",
4792                "severity": null,
4793                "title": "MCP server unavailable"
4794            }),
4795        ] {
4796            assert!(serde_json::from_str::<SessionUpdate>(&malformed.to_string()).is_err());
4797            assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
4798        }
4799    }
4800
4801    #[test]
4802    fn notice_tolerates_invalid_optional_display_fields_and_custom_severity() {
4803        use serde_json::json;
4804
4805        let wire = json!({
4806            "sessionUpdate": "notice",
4807            "severity": "_custom",
4808            "title": "Provider degraded",
4809            "description": 42,
4810            "_meta": false
4811        });
4812        let SessionUpdate::Notice(notice) = serde_json::from_value(wire.clone()).unwrap() else {
4813            panic!("expected notice");
4814        };
4815        assert_eq!(
4816            serde_json::from_str::<SessionUpdate>(&wire.to_string()).unwrap(),
4817            SessionUpdate::Notice(notice.clone())
4818        );
4819        assert_eq!(notice.severity, NoticeSeverity::Other("_custom".into()));
4820        assert_eq!(notice.description, None);
4821        assert_eq!(notice.meta, None);
4822        assert_eq!(
4823            serde_json::to_value(notice).unwrap(),
4824            json!({"severity": "_custom", "title": "Provider degraded"})
4825        );
4826    }
4827
4828    #[cfg(feature = "schemars")]
4829    #[test]
4830    fn notice_schema_preserves_required_fields_and_excludes_unknown_fallback() {
4831        use serde_json::json;
4832
4833        let schema = serde_json::to_value(schemars::schema_for!(Notice)).unwrap();
4834        assert_eq!(schema["required"], json!(["severity", "title"]));
4835        assert_eq!(schema["properties"]["title"]["minLength"], 1);
4836        for field in ["description", "_meta"] {
4837            assert!(
4838                schema["properties"][field]["type"]
4839                    .as_array()
4840                    .unwrap()
4841                    .contains(&json!("null"))
4842            );
4843        }
4844        let fallback = serde_json::to_value(schemars::schema_for!(OtherSessionUpdate)).unwrap();
4845        assert!(
4846            fallback["not"]["anyOf"]
4847                .as_array()
4848                .unwrap()
4849                .iter()
4850                .any(|variant| variant["properties"]["sessionUpdate"]["const"] == "notice")
4851        );
4852    }
4853
4854    #[test]
4855    fn compaction_updates_preserve_patch_and_open_status_semantics() {
4856        use serde_json::json;
4857
4858        assert_eq!(
4859            serde_json::to_value(SessionUpdate::CompactionUpdate(
4860                CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
4861                    ContentBlock::Text(crate::v2::TextContent::new("retained")),
4862                ]),
4863            ))
4864            .unwrap(),
4865            json!({
4866                "sessionUpdate": "compaction_update",
4867                "compactionId": "cmp_001",
4868                "status": "completed",
4869                "summary": [{ "type": "text", "text": "retained" }]
4870            })
4871        );
4872
4873        let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
4874            "sessionUpdate": "compaction_update",
4875            "compactionId": "cmp_001",
4876            "status": "paused",
4877            "summary": null
4878        }))
4879        .unwrap() else {
4880            panic!("expected compaction update");
4881        };
4882        assert_eq!(update.status, CompactionStatus::Other("paused".into()));
4883        assert!(update.summary.is_null());
4884        assert!(update.error.is_undefined());
4885    }
4886
4887    #[test]
4888    fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
4889        use serde_json::json;
4890
4891        assert!(
4892            serde_json::from_value::<SessionUpdate>(json!({
4893                "sessionUpdate": "compaction_update",
4894                "status": "completed"
4895            }))
4896            .is_err()
4897        );
4898        assert!(
4899            serde_json::from_value::<SessionUpdate>(json!({
4900                "sessionUpdate": "compaction_summary_chunk",
4901                "compactionId": "cmp_001"
4902            }))
4903            .is_err()
4904        );
4905    }
4906
4907    #[test]
4908    fn test_elicitation_capability_semantics() {
4909        use serde_json::json;
4910
4911        let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
4912        assert!(unsupported.elicitation.is_none());
4913
4914        let null: ClientCapabilities =
4915            serde_json::from_value(json!({ "elicitation": null })).unwrap();
4916        assert!(null.elicitation.is_none());
4917
4918        let malformed: ClientCapabilities =
4919            serde_json::from_value(json!({ "elicitation": false })).unwrap();
4920        assert!(malformed.elicitation.is_none());
4921
4922        let empty: ClientCapabilities =
4923            serde_json::from_value(json!({ "elicitation": {} })).unwrap();
4924        let empty = empty.elicitation.expect("present capability");
4925        assert!(!empty.supports_form());
4926        assert!(!empty.supports_url());
4927
4928        let form_only: ClientCapabilities = serde_json::from_value(json!({
4929            "elicitation": { "form": {} }
4930        }))
4931        .unwrap();
4932        let form_only = form_only.elicitation.expect("advertised capability");
4933        assert!(form_only.supports_form());
4934        assert!(!form_only.supports_url());
4935
4936        let url_only: ClientCapabilities = serde_json::from_value(json!({
4937            "elicitation": { "url": {} }
4938        }))
4939        .unwrap();
4940        let url_only = url_only.elicitation.expect("advertised capability");
4941        assert!(!url_only.supports_form());
4942        assert!(url_only.supports_url());
4943
4944        let both: ClientCapabilities = serde_json::from_value(json!({
4945            "elicitation": { "form": {}, "url": {} }
4946        }))
4947        .unwrap();
4948        let both = both.elicitation.expect("advertised capability");
4949        assert!(both.supports_form());
4950        assert!(both.supports_url());
4951    }
4952
4953    #[test]
4954    fn test_elicitation_method_routing_and_envelopes() {
4955        use serde_json::json;
4956
4957        assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
4958        assert_eq!(
4959            CLIENT_METHOD_NAMES.elicitation_complete,
4960            "elicitation/complete"
4961        );
4962
4963        let request =
4964            AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
4965                crate::v2::ElicitationFormMode::new(
4966                    crate::v2::ElicitationSessionScope::new("sess_1"),
4967                    crate::v2::ElicitationSchema::new(),
4968                ),
4969                "Choose a value",
4970            )));
4971        assert_eq!(request.method(), "elicitation/create");
4972        let method = Arc::from(request.method());
4973        let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
4974            id: crate::v2::RequestId::Number(7),
4975            method,
4976            params: Some(request),
4977        });
4978        assert_eq!(
4979            serde_json::to_value(request).unwrap(),
4980            json!({
4981                "jsonrpc": "2.0",
4982                "id": 7,
4983                "method": "elicitation/create",
4984                "params": {
4985                    "mode": "form",
4986                    "sessionId": "sess_1",
4987                    "message": "Choose a value",
4988                    "requestedSchema": { "type": "object", "properties": {} }
4989                }
4990            })
4991        );
4992
4993        let notification = AgentNotification::CompleteElicitationNotification(Box::new(
4994            CompleteElicitationNotification::new("elic_1"),
4995        ));
4996        assert_eq!(notification.method(), "elicitation/complete");
4997        let method = Arc::from(notification.method());
4998        let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
4999            method,
5000            params: Some(notification),
5001        });
5002        assert_eq!(
5003            serde_json::to_value(notification).unwrap(),
5004            json!({
5005                "jsonrpc": "2.0",
5006                "method": "elicitation/complete",
5007                "params": { "elicitationId": "elic_1" }
5008            })
5009        );
5010    }
5011
5012    #[test]
5013    fn test_client_capabilities_auth_defaults_on_malformed_value() {
5014        use serde_json::json;
5015
5016        let capabilities: ClientCapabilities = serde_json::from_value(json!({
5017            "auth": false
5018        }))
5019        .unwrap();
5020
5021        assert_eq!(capabilities.auth, None);
5022    }
5023
5024    #[test]
5025    fn test_serialization_behavior() {
5026        use serde_json::json;
5027
5028        assert_eq!(
5029            serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
5030            SessionInfoUpdate {
5031                title: MaybeUndefined::Undefined,
5032                updated_at: MaybeUndefined::Undefined,
5033                meta: MaybeUndefined::Undefined
5034            }
5035        );
5036        assert_eq!(
5037            serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
5038                .unwrap(),
5039            SessionInfoUpdate {
5040                title: MaybeUndefined::Null,
5041                updated_at: MaybeUndefined::Null,
5042                meta: MaybeUndefined::Undefined
5043            }
5044        );
5045        assert_eq!(
5046            serde_json::from_value::<SessionInfoUpdate>(
5047                json!({"title": "title", "updatedAt": "timestamp"})
5048            )
5049            .unwrap(),
5050            SessionInfoUpdate {
5051                title: MaybeUndefined::Value("title".to_string()),
5052                updated_at: MaybeUndefined::Value("timestamp".to_string()),
5053                meta: MaybeUndefined::Undefined
5054            }
5055        );
5056
5057        let clear_meta =
5058            serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
5059        assert_eq!(clear_meta.meta, MaybeUndefined::Null);
5060
5061        let mut meta = Meta::new();
5062        meta.insert("source".to_string(), json!("session-info"));
5063
5064        assert_eq!(
5065            serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
5066                "source": "session-info"
5067            }}))
5068            .unwrap()
5069            .meta,
5070            MaybeUndefined::Value(meta.clone())
5071        );
5072
5073        assert_eq!(
5074            serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
5075            json!({})
5076        );
5077
5078        assert_eq!(
5079            serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
5080            json!({"_meta": null})
5081        );
5082
5083        assert_eq!(
5084            serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
5085            json!({"_meta": {
5086                "source": "session-info"
5087            }})
5088        );
5089        assert_eq!(
5090            serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
5091            json!({"title": "title"})
5092        );
5093        assert_eq!(
5094            serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
5095            json!({"title": null})
5096        );
5097        assert_eq!(
5098            serde_json::to_value(
5099                SessionInfoUpdate::new()
5100                    .title("title")
5101                    .title(MaybeUndefined::Undefined)
5102            )
5103            .unwrap(),
5104            json!({})
5105        );
5106    }
5107
5108    #[test]
5109    fn test_content_chunk_message_id_serialization() {
5110        use serde_json::json;
5111
5112        assert_eq!(
5113            serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
5114                ContentBlock::Text(crate::v2::TextContent::new("Hello")),
5115                "msg_agent_c42b9",
5116            )))
5117            .unwrap(),
5118            json!({
5119                "sessionUpdate": "agent_message_chunk",
5120                "messageId": "msg_agent_c42b9",
5121                "content": {
5122                    "type": "text",
5123                    "text": "Hello"
5124                }
5125            })
5126        );
5127
5128        let err = serde_json::from_value::<ContentChunk>(json!({
5129            "content": {
5130                "type": "text",
5131                "text": "Hello"
5132            }
5133        }))
5134        .unwrap_err();
5135
5136        assert!(err.to_string().contains("messageId"), "{err}");
5137    }
5138
5139    #[test]
5140    fn test_tool_call_content_chunk_serialization() {
5141        use serde_json::json;
5142
5143        assert_eq!(
5144            serde_json::to_value(SessionUpdate::ToolCallContentChunk(
5145                ToolCallContentChunk::new(
5146                    "call_001",
5147                    crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
5148                )
5149            ))
5150            .unwrap(),
5151            json!({
5152                "sessionUpdate": "tool_call_content_chunk",
5153                "toolCallId": "call_001",
5154                "content": {
5155                    "type": "content",
5156                    "content": {
5157                        "type": "text",
5158                        "text": "partial output"
5159                    }
5160                }
5161            })
5162        );
5163
5164        let err = serde_json::from_value::<ToolCallContentChunk>(json!({
5165            "content": {
5166                "type": "content",
5167                "content": {
5168                    "type": "text",
5169                    "text": "partial output"
5170                }
5171            }
5172        }))
5173        .unwrap_err();
5174
5175        assert!(err.to_string().contains("toolCallId"), "{err}");
5176    }
5177
5178    #[test]
5179    fn test_full_message_serialization() {
5180        use serde_json::json;
5181
5182        assert_eq!(
5183            serde_json::to_value(SessionUpdate::UserMessage(
5184                UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
5185                    crate::v2::TextContent::new("Hello")
5186                )])
5187            ))
5188            .unwrap(),
5189            json!({
5190                "sessionUpdate": "user_message",
5191                "messageId": "msg_user_8f7a1",
5192                "content": [
5193                    {
5194                        "type": "text",
5195                        "text": "Hello"
5196                    }
5197                ]
5198            })
5199        );
5200
5201        assert_eq!(
5202            serde_json::to_value(SessionUpdate::AgentMessage(
5203                AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
5204                    crate::v2::TextContent::new("Hello")
5205                )])
5206            ))
5207            .unwrap(),
5208            json!({
5209                "sessionUpdate": "agent_message",
5210                "messageId": "msg_agent_c42b9",
5211                "content": [
5212                    {
5213                        "type": "text",
5214                        "text": "Hello"
5215                    }
5216                ]
5217            })
5218        );
5219
5220        assert_eq!(
5221            serde_json::to_value(SessionUpdate::AgentThought(
5222                AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
5223                    crate::v2::TextContent::new("Need to inspect the call sites first.")
5224                )])
5225            ))
5226            .unwrap(),
5227            json!({
5228                "sessionUpdate": "agent_thought",
5229                "messageId": "msg_thought_a12",
5230                "content": [
5231                    {
5232                        "type": "text",
5233                        "text": "Need to inspect the call sites first."
5234                    }
5235                ]
5236            })
5237        );
5238    }
5239
5240    #[test]
5241    fn test_message_upsert_serialization() {
5242        use serde_json::json;
5243
5244        assert_eq!(
5245            serde_json::to_value(SessionUpdate::UserMessage(
5246                UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
5247            ))
5248            .unwrap(),
5249            json!({
5250                "sessionUpdate": "user_message",
5251                "messageId": "msg_empty",
5252                "content": []
5253            })
5254        );
5255
5256        let empty = serde_json::from_value::<UserMessage>(json!({
5257            "messageId": "msg_empty",
5258            "content": []
5259        }))
5260        .unwrap();
5261        assert!(matches!(
5262            empty.content,
5263            MaybeUndefined::Value(ref content) if content.is_empty()
5264        ));
5265
5266        let patch = serde_json::from_value::<AgentMessage>(json!({
5267            "messageId": "msg_agent_c42b9"
5268        }))
5269        .unwrap();
5270        assert_eq!(patch.content, MaybeUndefined::Undefined);
5271        assert_eq!(patch.meta, MaybeUndefined::Undefined);
5272
5273        let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
5274            "messageId": "msg_agent_c42b9",
5275            "_meta": false
5276        }))
5277        .unwrap();
5278        assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
5279
5280        let patch = serde_json::from_value::<AgentThought>(json!({
5281            "messageId": "msg_thought_a12"
5282        }))
5283        .unwrap();
5284        assert_eq!(patch.content, MaybeUndefined::Undefined);
5285
5286        let clear = serde_json::from_value::<UserMessage>(json!({
5287            "messageId": "msg_user_8f7a1",
5288            "content": null
5289        }))
5290        .unwrap();
5291        assert_eq!(clear.content, MaybeUndefined::Null);
5292
5293        let clear_meta = serde_json::from_value::<UserMessage>(json!({
5294            "messageId": "msg_user_8f7a1",
5295            "_meta": null
5296        }))
5297        .unwrap();
5298        assert_eq!(clear_meta.meta, MaybeUndefined::Null);
5299
5300        let mut meta = Meta::new();
5301        meta.insert("source".to_string(), json!("replay"));
5302
5303        assert_eq!(
5304            serde_json::to_value(SessionUpdate::UserMessage(
5305                UserMessage::new("msg_user_8f7a1").meta(meta)
5306            ))
5307            .unwrap(),
5308            json!({
5309                "sessionUpdate": "user_message",
5310                "messageId": "msg_user_8f7a1",
5311                "_meta": {
5312                    "source": "replay"
5313                }
5314            })
5315        );
5316
5317        assert_eq!(
5318            serde_json::to_value(SessionUpdate::UserMessage(
5319                UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
5320            ))
5321            .unwrap(),
5322            json!({
5323                "sessionUpdate": "user_message",
5324                "messageId": "msg_user_8f7a1",
5325                "_meta": null
5326            })
5327        );
5328    }
5329
5330    #[test]
5331    fn test_usage_update_serialization() {
5332        use serde_json::json;
5333
5334        assert_eq!(
5335            serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
5336                53_000, 200_000
5337            )))
5338            .unwrap(),
5339            json!({
5340                "sessionUpdate": "usage_update",
5341                "used": 53000,
5342                "size": 200_000
5343            })
5344        );
5345
5346        assert_eq!(
5347            serde_json::to_value(SessionUpdate::UsageUpdate(
5348                UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
5349            ))
5350            .unwrap(),
5351            json!({
5352                "sessionUpdate": "usage_update",
5353                "used": 53000,
5354                "size": 200_000,
5355                "cost": {
5356                    "amount": 0.045,
5357                    "currency": "USD"
5358                }
5359            })
5360        );
5361
5362        let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
5363            "sessionUpdate": "usage_update",
5364            "used": 53000,
5365            "size": 200_000,
5366            "cost": null
5367        }))
5368        .unwrap() else {
5369            panic!("expected usage update");
5370        };
5371
5372        assert_eq!(update.cost, MaybeUndefined::Null);
5373
5374        let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
5375            "sessionUpdate": "usage_update",
5376            "used": 53000,
5377            "size": 200_000
5378        }))
5379        .unwrap() else {
5380            panic!("expected usage update");
5381        };
5382
5383        assert_eq!(update.cost, MaybeUndefined::Undefined);
5384    }
5385
5386    #[test]
5387    fn error_stop_reason_carries_failure_beside_discriminator() {
5388        use crate::v2::{Error, ErrorStopReason};
5389        use serde_json::json;
5390
5391        let update =
5392            SessionUpdate::StateUpdate(StateUpdate::Idle(IdleStateUpdate::new().stop_reason(
5393                StopReason::from(ErrorStopReason::new().error(Error::auth_required())),
5394            )));
5395        let wire = json!({
5396            "sessionUpdate": "state_update",
5397            "state": "idle",
5398            "stopReason": "error",
5399            "error": { "code": -32000, "message": "Authentication required" }
5400        });
5401        assert_eq!(serde_json::to_value(&update).unwrap(), wire);
5402        assert_eq!(
5403            serde_json::from_value::<SessionUpdate>(wire).unwrap(),
5404            update
5405        );
5406
5407        let with_data =
5408            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::from(
5409                ErrorStopReason::new().error(
5410                    Error::new(-32099, "Provider unavailable").data(json!({ "status": 503 })),
5411                ),
5412            )));
5413        let wire = json!({
5414            "state": "idle",
5415            "stopReason": "error",
5416            "error": {
5417                "code": -32099,
5418                "message": "Provider unavailable",
5419                "data": { "status": 503 }
5420            }
5421        });
5422        assert_eq!(serde_json::to_value(&with_data).unwrap(), wire);
5423        assert_eq!(
5424            serde_json::from_value::<StateUpdate>(wire).unwrap(),
5425            with_data
5426        );
5427
5428        // Missing, null, and malformed details leave a failure without details;
5429        // the stop reason still ends foreground work.
5430        let without_details = StateUpdate::Idle(
5431            IdleStateUpdate::new().stop_reason(StopReason::from(ErrorStopReason::new())),
5432        );
5433        for error in [
5434            None,
5435            Some(json!(null)),
5436            Some(json!("failed")),
5437            Some(json!({ "message": "missing code" })),
5438        ] {
5439            let mut wire = json!({ "state": "idle", "stopReason": "error" });
5440            if let Some(error) = error {
5441                wire["error"] = error;
5442            }
5443            assert_eq!(
5444                serde_json::from_value::<StateUpdate>(wire).unwrap(),
5445                without_details
5446            );
5447        }
5448        assert_eq!(
5449            serde_json::to_value(&without_details).unwrap(),
5450            json!({ "state": "idle", "stopReason": "error" })
5451        );
5452
5453        // `error` is a field of the `error` stop reason only. Beside any other
5454        // stop reason it is an unknown field: the update stays readable and the
5455        // error has nowhere to go.
5456        let parsed: StateUpdate = serde_json::from_value(json!({
5457            "state": "idle",
5458            "stopReason": "end_turn",
5459            "error": { "code": -32603, "message": "Internal error" }
5460        }))
5461        .unwrap();
5462        assert_eq!(
5463            parsed,
5464            StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn))
5465        );
5466    }
5467
5468    #[test]
5469    fn flattened_stop_reason_keeps_open_enum_tolerance() {
5470        use crate::v2::OtherStopReason;
5471        use serde_json::json;
5472
5473        let meta: Meta = [("source".into(), json!("test"))].into_iter().collect();
5474
5475        // Omitted, null, and malformed stop reasons all mean no stop reason, as
5476        // when `stopReason` was a separate default-on-error field.
5477        for stop_reason in [None, Some(json!(null)), Some(json!(true)), Some(json!({}))] {
5478            let mut wire = json!({ "state": "idle", "_meta": { "source": "test" } });
5479            if let Some(stop_reason) = stop_reason {
5480                wire["stopReason"] = stop_reason;
5481            }
5482            let StateUpdate::Idle(idle) = serde_json::from_value(wire).unwrap() else {
5483                panic!("expected idle state update");
5484            };
5485            assert_eq!(idle.stop_reason, None);
5486            assert_eq!(idle.meta, Some(meta.clone()));
5487        }
5488
5489        // Unknown stop reasons keep their own fields for proxies and replay,
5490        // while the idle update's fields stay on the idle update.
5491        let wire = json!({
5492            "state": "idle",
5493            "stopReason": "_paused",
5494            "resumeAfter": 30,
5495            "_meta": { "source": "test" }
5496        });
5497        let parsed: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
5498        let expected = StateUpdate::Idle(
5499            IdleStateUpdate::new()
5500                .stop_reason(StopReason::Other(OtherStopReason::new(
5501                    "_paused",
5502                    [("resumeAfter".into(), json!(30))].into_iter().collect(),
5503                )))
5504                .meta(meta),
5505        );
5506        assert_eq!(parsed, expected);
5507        assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
5508    }
5509
5510    #[test]
5511    fn known_stop_reasons_are_never_read_as_other() {
5512        use crate::v2::{ErrorStopReason, OtherStopReason};
5513        use serde_json::json;
5514
5515        let known = [
5516            StopReason::EndTurn,
5517            StopReason::MaxTokens,
5518            StopReason::MaxTurnRequests,
5519            StopReason::Refusal,
5520            StopReason::Cancelled,
5521            StopReason::Error(ErrorStopReason::new()),
5522        ];
5523        // Adding a variant breaks this match, so `known` stays complete.
5524        for stop_reason in &known {
5525            match stop_reason {
5526                StopReason::EndTurn
5527                | StopReason::MaxTokens
5528                | StopReason::MaxTurnRequests
5529                | StopReason::Refusal
5530                | StopReason::Cancelled
5531                | StopReason::Error(_)
5532                | StopReason::Other(_) => {}
5533            }
5534        }
5535
5536        for stop_reason in known {
5537            let update = StateUpdate::Idle(IdleStateUpdate::new().stop_reason(stop_reason));
5538            let wire = serde_json::to_value(&update).unwrap();
5539            assert_eq!(
5540                serde_json::from_value::<StateUpdate>(wire.clone()).unwrap(),
5541                update
5542            );
5543            assert!(
5544                serde_json::from_value::<OtherStopReason>(
5545                    json!({ "stopReason": wire["stopReason"] })
5546                )
5547                .is_err(),
5548                "{} must not be accepted as a custom stop reason",
5549                wire["stopReason"]
5550            );
5551        }
5552    }
5553
5554    #[test]
5555    fn unknown_stop_reason_leaves_usage_on_the_idle_update() {
5556        use serde_json::json;
5557
5558        let usage = json!({ "totalTokens": 10, "inputTokens": 6, "outputTokens": 4 });
5559        let wire = json!({ "state": "idle", "stopReason": "_paused", "usage": usage });
5560        let StateUpdate::Idle(idle) = serde_json::from_value(wire.clone()).unwrap() else {
5561            panic!("expected idle state update");
5562        };
5563        let Some(StopReason::Other(other)) = &idle.stop_reason else {
5564            panic!("expected a custom stop reason");
5565        };
5566        assert!(other.fields.is_empty());
5567
5568        #[cfg(feature = "unstable_end_turn_token_usage")]
5569        {
5570            assert!(idle.usage.is_some());
5571            assert_eq!(serde_json::to_value(&idle).unwrap()["usage"], usage);
5572        }
5573        // Without the feature, `usage` is not a field of the idle update, and it
5574        // must not be smuggled through as a field of the stop reason either.
5575        #[cfg(not(feature = "unstable_end_turn_token_usage"))]
5576        assert!(serde_json::to_value(&idle).unwrap().get("usage").is_none());
5577    }
5578
5579    #[test]
5580    fn test_state_update_serialization() {
5581        use serde_json::json;
5582
5583        assert_eq!(
5584            serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
5585                RunningStateUpdate::new()
5586            )))
5587            .unwrap(),
5588            json!({
5589                "sessionUpdate": "state_update",
5590                "state": "running"
5591            })
5592        );
5593
5594        assert_eq!(
5595            serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
5596                IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
5597            )))
5598            .unwrap(),
5599            json!({
5600                "sessionUpdate": "state_update",
5601                "state": "idle",
5602                "stopReason": "end_turn"
5603            })
5604        );
5605
5606        let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
5607            "sessionUpdate": "state_update",
5608            "state": "requires_action"
5609        }))
5610        .unwrap() else {
5611            panic!("expected state update");
5612        };
5613
5614        assert!(matches!(update, StateUpdate::RequiresAction(_)));
5615
5616        let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
5617            "sessionUpdate": "state_update",
5618            "state": "idle",
5619            "stopReason": null
5620        }))
5621        .unwrap() else {
5622            panic!("expected idle state update");
5623        };
5624
5625        assert_eq!(update.stop_reason, None);
5626
5627        let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
5628            serde_json::from_value(json!({
5629                "sessionUpdate": "state_update",
5630                "state": "_paused",
5631                "label": "Paused"
5632            }))
5633            .unwrap()
5634        else {
5635            panic!("expected unknown state update");
5636        };
5637
5638        assert_eq!(update.state, "_paused");
5639        assert_eq!(update.fields["label"], json!("Paused"));
5640    }
5641
5642    #[test]
5643    fn session_update_preserves_unknown_variant() {
5644        use serde_json::json;
5645
5646        let update: SessionUpdate = serde_json::from_value(json!({
5647            "sessionUpdate": "_status_badge",
5648            "label": "Indexing",
5649            "progress": 0.5
5650        }))
5651        .unwrap();
5652
5653        let SessionUpdate::Other(unknown) = update else {
5654            panic!("expected unknown session update");
5655        };
5656
5657        assert_eq!(unknown.session_update, "_status_badge");
5658        assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
5659        assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
5660
5661        assert_eq!(
5662            serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
5663            json!({
5664                "sessionUpdate": "_status_badge",
5665                "label": "Indexing",
5666                "progress": 0.5
5667            })
5668        );
5669    }
5670
5671    #[test]
5672    fn terminal_session_updates_use_known_discriminators() {
5673        use serde_json::json;
5674
5675        assert_eq!(
5676            serde_json::to_value(SessionUpdate::TerminalUpdate(
5677                TerminalUpdate::new("term_1").command("cargo test")
5678            ))
5679            .unwrap(),
5680            json!({
5681                "sessionUpdate": "terminal_update",
5682                "terminalId": "term_1",
5683                "command": "cargo test"
5684            })
5685        );
5686        assert_eq!(
5687            serde_json::to_value(SessionUpdate::TerminalOutputChunk(
5688                TerminalOutputChunk::new("term_1", "dGVzdAo=")
5689            ))
5690            .unwrap(),
5691            json!({
5692                "sessionUpdate": "terminal_output_chunk",
5693                "terminalId": "term_1",
5694                "data": "dGVzdAo="
5695            })
5696        );
5697    }
5698
5699    #[test]
5700    fn session_update_does_not_hide_malformed_known_terminal_variants() {
5701        use serde_json::json;
5702
5703        assert!(
5704            serde_json::from_value::<SessionUpdate>(json!({
5705                "sessionUpdate": "terminal_update"
5706            }))
5707            .is_err()
5708        );
5709        assert!(
5710            serde_json::from_value::<SessionUpdate>(json!({
5711                "sessionUpdate": "terminal_output_chunk",
5712                "terminalId": "term_1"
5713            }))
5714            .is_err()
5715        );
5716    }
5717
5718    #[test]
5719    fn test_plan_update_serialization() {
5720        use serde_json::json;
5721
5722        let plan_update =
5723            SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
5724                "plan-1",
5725                vec![crate::v2::PlanEntry::new(
5726                    "Step 1",
5727                    crate::v2::PlanEntryPriority::High,
5728                    crate::v2::PlanEntryStatus::Pending,
5729                )],
5730            )));
5731
5732        assert_eq!(
5733            serde_json::to_value(plan_update).unwrap(),
5734            json!({
5735                "sessionUpdate": "plan_update",
5736                "plan": {
5737                    "type": "items",
5738                    "planId": "plan-1",
5739                    "entries": [
5740                        {
5741                            "content": "Step 1",
5742                            "priority": "high",
5743                            "status": "pending"
5744                        }
5745                    ]
5746                }
5747            })
5748        );
5749    }
5750
5751    #[cfg(feature = "unstable_plan_operations")]
5752    #[test]
5753    fn test_plan_removed_serialization() {
5754        use serde_json::json;
5755
5756        assert_eq!(
5757            serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
5758            json!({
5759                "sessionUpdate": "plan_removed",
5760                "planId": "plan-1"
5761            })
5762        );
5763    }
5764
5765    #[test]
5766    fn available_command_uses_type_discriminator() {
5767        use serde_json::json;
5768
5769        let command = AvailableCommand::Command(
5770            Command::new("web", "Search the web")
5771                .input(AvailableCommandInput::Text(TextCommandInput::new("Query"))),
5772        );
5773
5774        let json = serde_json::to_value(&command).unwrap();
5775        assert_eq!(
5776            json,
5777            json!({
5778                "type": "command",
5779                "name": "web",
5780                "description": "Search the web",
5781                "input": { "type": "text", "hint": "Query" }
5782            })
5783        );
5784
5785        let roundtripped: AvailableCommand = serde_json::from_value(json).unwrap();
5786        assert_eq!(roundtripped, command);
5787        assert_eq!(roundtripped.name(), "web");
5788        assert_eq!(roundtripped.description(), "Search the web");
5789    }
5790
5791    #[test]
5792    fn available_command_preserves_unknown_typed_variant() {
5793        use serde_json::json;
5794
5795        let payload = json!({
5796            "type": "_skill",
5797            "name": "pdf-processing",
5798            "description": "Extract text from PDFs",
5799            "uri": "skill://pdf-processing/SKILL.md",
5800            "_meta": { "example.com/source": "mcp" }
5801        });
5802        let command: AvailableCommand = serde_json::from_value(payload.clone()).unwrap();
5803
5804        assert_eq!(command.name(), "pdf-processing");
5805        assert_eq!(command.description(), "Extract text from PDFs");
5806        assert!(command.meta().is_some());
5807
5808        let AvailableCommand::Other(unknown) = &command else {
5809            panic!("expected unknown command");
5810        };
5811        assert_eq!(unknown.type_, "_skill");
5812        assert_eq!(
5813            unknown.fields.get("uri"),
5814            Some(&json!("skill://pdf-processing/SKILL.md"))
5815        );
5816        assert_eq!(serde_json::to_value(&command).unwrap(), payload);
5817    }
5818
5819    #[test]
5820    fn available_command_unknown_tolerates_invalid_meta() {
5821        use serde_json::json;
5822
5823        let command: AvailableCommand = serde_json::from_value(json!({
5824            "type": "_skill",
5825            "name": "pdf-processing",
5826            "description": "Extract text from PDFs",
5827            "_meta": "oops"
5828        }))
5829        .unwrap();
5830
5831        assert!(matches!(command, AvailableCommand::Other(_)));
5832        assert!(command.meta().is_none());
5833    }
5834
5835    #[test]
5836    fn available_command_unknown_does_not_hide_malformed_command_variant() {
5837        use serde_json::json;
5838
5839        // A missing discriminator is rejected rather than defaulting to `command`.
5840        assert!(
5841            serde_json::from_value::<AvailableCommand>(json!({
5842                "name": "test",
5843                "description": "Run project tests"
5844            }))
5845            .is_err()
5846        );
5847        assert!(
5848            serde_json::from_value::<AvailableCommand>(json!({
5849                "type": "command",
5850                "name": "test"
5851            }))
5852            .is_err()
5853        );
5854        assert!(
5855            serde_json::from_value::<OtherAvailableCommand>(json!({
5856                "type": "command",
5857                "name": "test",
5858                "description": "Run project tests"
5859            }))
5860            .is_err()
5861        );
5862        assert!(
5863            serde_json::from_value::<AvailableCommand>(json!({
5864                "type": "_skill",
5865                "name": "pdf-processing"
5866            }))
5867            .is_err()
5868        );
5869    }
5870
5871    #[test]
5872    fn available_command_input_preserves_unknown_typed_variant() {
5873        use serde_json::json;
5874
5875        let input: AvailableCommandInput = serde_json::from_value(json!({
5876            "type": "_choices",
5877            "hint": "Pick one",
5878            "options": ["fast", "careful"]
5879        }))
5880        .unwrap();
5881
5882        let AvailableCommandInput::Other(unknown) = input else {
5883            panic!("expected unknown command input");
5884        };
5885
5886        assert_eq!(unknown.type_, "_choices");
5887        assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
5888        assert_eq!(
5889            unknown.fields.get("options"),
5890            Some(&json!(["fast", "careful"]))
5891        );
5892        assert_eq!(
5893            serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
5894            json!({
5895                "type": "_choices",
5896                "hint": "Pick one",
5897                "options": ["fast", "careful"]
5898            })
5899        );
5900    }
5901
5902    #[test]
5903    fn available_command_input_text_uses_type_discriminator() {
5904        use serde_json::json;
5905
5906        let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
5907
5908        let json = serde_json::to_value(&input).unwrap();
5909        assert_eq!(
5910            json,
5911            json!({
5912                "type": "text",
5913                "hint": "Describe changes"
5914            })
5915        );
5916
5917        let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
5918        assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
5919    }
5920
5921    #[test]
5922    fn request_permission_subject_tool_call_uses_type_discriminator() {
5923        use serde_json::json;
5924
5925        let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
5926
5927        let json = serde_json::to_value(&subject).unwrap();
5928        assert_eq!(
5929            json,
5930            json!({
5931                "type": "tool_call",
5932                "toolCall": {
5933                    "toolCallId": "call_001"
5934                }
5935            })
5936        );
5937
5938        let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5939        assert!(matches!(
5940            roundtripped,
5941            RequestPermissionSubject::ToolCall(_)
5942        ));
5943    }
5944
5945    #[test]
5946    fn request_permission_subject_command_uses_type_discriminator() {
5947        use serde_json::json;
5948
5949        let mut meta = Meta::new();
5950        meta.insert("source".to_string(), json!("shell"));
5951        let subject = RequestPermissionSubject::from(
5952            CommandPermissionSubject::new("cargo test", "/workspace/project")
5953                .tool_call_id("call_001")
5954                .terminal_id("term_1")
5955                .meta(meta),
5956        );
5957
5958        let json = serde_json::to_value(&subject).unwrap();
5959        assert_eq!(
5960            json,
5961            json!({
5962                "type": "command",
5963                "command": "cargo test",
5964                "cwd": "/workspace/project",
5965                "toolCallId": "call_001",
5966                "terminalId": "term_1",
5967                "_meta": {
5968                    "source": "shell"
5969                }
5970            })
5971        );
5972
5973        let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5974        assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
5975    }
5976
5977    #[test]
5978    fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
5979        use serde_json::json;
5980
5981        let subject: RequestPermissionSubject = serde_json::from_value(json!({
5982            "type": "command",
5983            "command": "cargo test",
5984            "cwd": "/workspace/project",
5985            "toolCallId": null,
5986            "terminalId": null,
5987            "_meta": null
5988        }))
5989        .unwrap();
5990
5991        let RequestPermissionSubject::Command(subject) = subject else {
5992            panic!("expected command permission subject");
5993        };
5994        assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
5995        assert_eq!(subject.tool_call_id, None);
5996        assert_eq!(subject.terminal_id, None);
5997        assert_eq!(subject.meta, None);
5998        assert_eq!(
5999            serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
6000            json!({
6001                "type": "command",
6002                "command": "cargo test",
6003                "cwd": "/workspace/project"
6004            })
6005        );
6006    }
6007
6008    #[test]
6009    fn request_permission_subject_preserves_unknown_variant() {
6010        use serde_json::json;
6011
6012        let subject: RequestPermissionSubject = serde_json::from_value(json!({
6013            "type": "_review",
6014            "reason": "needs-review",
6015            "retryAfterSeconds": 30
6016        }))
6017        .unwrap();
6018
6019        let RequestPermissionSubject::Other(unknown) = subject else {
6020            panic!("expected unknown permission subject");
6021        };
6022
6023        assert_eq!(unknown.type_, "_review");
6024        assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
6025        assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
6026        assert_eq!(
6027            serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
6028            json!({
6029                "type": "_review",
6030                "reason": "needs-review",
6031                "retryAfterSeconds": 30
6032            })
6033        );
6034    }
6035
6036    #[test]
6037    fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
6038        use serde_json::json;
6039
6040        assert!(
6041            serde_json::from_value::<RequestPermissionSubject>(json!({
6042                "type": "tool_call"
6043            }))
6044            .is_err()
6045        );
6046        assert!(
6047            serde_json::from_value::<RequestPermissionSubject>(json!({
6048                "type": 1
6049            }))
6050            .is_err()
6051        );
6052        assert!(
6053            serde_json::from_value::<RequestPermissionSubject>(json!({
6054                "type": "command",
6055                "cwd": "/workspace/project"
6056            }))
6057            .is_err()
6058        );
6059        assert!(
6060            serde_json::from_value::<RequestPermissionSubject>(json!({
6061                "type": "command",
6062                "command": "cargo test"
6063            }))
6064            .is_err()
6065        );
6066        assert!(
6067            serde_json::from_value::<RequestPermissionSubject>(json!({
6068                "type": "command",
6069                "command": "cargo test",
6070                "cwd": null
6071            }))
6072            .is_err()
6073        );
6074    }
6075
6076    #[test]
6077    fn request_permission_title_and_description_are_separate_from_tool_call_content() {
6078        use serde_json::json;
6079
6080        let request =
6081            RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
6082                .description("Allow this tool to edit src/main.rs?")
6083                .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
6084                    "call_001",
6085                )));
6086
6087        assert_eq!(
6088            serde_json::to_value(request).unwrap(),
6089            json!({
6090                "sessionId": "sess_abc123def456",
6091                "title": "Approve file edit?",
6092                "description": "Allow this tool to edit src/main.rs?",
6093                "subject": {
6094                    "type": "tool_call",
6095                    "toolCall": {
6096                        "toolCallId": "call_001"
6097                    }
6098                },
6099                "options": []
6100            })
6101        );
6102    }
6103
6104    #[test]
6105    fn request_permission_requires_title_and_allows_missing_subject() {
6106        use serde_json::json;
6107
6108        let request = RequestPermissionRequest::new(
6109            "sess_abc123def456",
6110            "Approve elevated permissions?",
6111            Vec::new(),
6112        );
6113
6114        assert_eq!(
6115            serde_json::to_value(request).unwrap(),
6116            json!({
6117                "sessionId": "sess_abc123def456",
6118                "title": "Approve elevated permissions?",
6119                "options": []
6120            })
6121        );
6122
6123        let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
6124            "sessionId": "sess_abc123def456",
6125            "title": "Approve elevated permissions?",
6126            "options": []
6127        }))
6128        .unwrap();
6129        assert!(missing_subject.subject.is_none());
6130
6131        let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
6132            "sessionId": "sess_abc123def456",
6133            "title": "Approve elevated permissions?",
6134            "subject": null,
6135            "options": []
6136        }))
6137        .unwrap();
6138        assert!(null_subject.subject.is_none());
6139
6140        assert!(
6141            serde_json::from_value::<RequestPermissionRequest>(json!({
6142                "sessionId": "sess_abc123def456",
6143                "options": []
6144            }))
6145            .is_err()
6146        );
6147    }
6148
6149    #[test]
6150    fn request_permission_outcome_preserves_unknown_variant() {
6151        use serde_json::json;
6152
6153        let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
6154            "outcome": "_defer",
6155            "reason": "needs-review",
6156            "retryAfterSeconds": 30
6157        }))
6158        .unwrap();
6159
6160        let RequestPermissionOutcome::Other(unknown) = outcome else {
6161            panic!("expected unknown permission outcome");
6162        };
6163
6164        assert_eq!(unknown.outcome, "_defer");
6165        assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
6166        assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
6167        assert_eq!(
6168            serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
6169            json!({
6170                "outcome": "_defer",
6171                "reason": "needs-review",
6172                "retryAfterSeconds": 30
6173            })
6174        );
6175    }
6176
6177    #[test]
6178    fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
6179        use serde_json::json;
6180
6181        assert!(
6182            serde_json::from_value::<RequestPermissionOutcome>(json!({
6183                "outcome": "selected"
6184            }))
6185            .is_err()
6186        );
6187        assert!(
6188            serde_json::from_value::<RequestPermissionOutcome>(json!({
6189                "outcome": 1
6190            }))
6191            .is_err()
6192        );
6193    }
6194
6195    #[test]
6196    fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
6197        use serde_json::json;
6198
6199        assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
6200        assert!(
6201            serde_json::from_value::<AvailableCommandInput>(json!({
6202                "hint": "Pick one"
6203            }))
6204            .is_err()
6205        );
6206        assert!(
6207            serde_json::from_value::<AvailableCommandInput>(json!({
6208                "type": 1,
6209                "hint": "Pick one"
6210            }))
6211            .is_err()
6212        );
6213        assert!(
6214            serde_json::from_value::<OtherAvailableCommandInput>(json!({
6215                "type": "text",
6216                "hint": "Pick one"
6217            }))
6218            .is_err()
6219        );
6220    }
6221
6222    #[cfg(feature = "unstable_mcp_over_acp")]
6223    #[test]
6224    fn test_agent_mcp_request_method_names() {
6225        use serde_json::json;
6226
6227        let params: serde_json::Map<String, serde_json::Value> =
6228            [("cursor".to_string(), json!("abc"))].into_iter().collect();
6229
6230        assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
6231        assert_eq!(
6232            AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
6233                "server-1",
6234                "req-1",
6235                "tools/list"
6236            )))
6237            .method(),
6238            "mcp/message"
6239        );
6240        assert_eq!(
6241            serde_json::to_value(
6242                MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
6243            )
6244            .unwrap(),
6245            json!({
6246                "serverId": "server-1",
6247                "requestId": "req-1",
6248                "method": "tools/list",
6249                "params": { "cursor": "abc" }
6250            })
6251        );
6252
6253        let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
6254            "serverId": "server-1",
6255            "requestId": "req-1",
6256            "method": "tools/list",
6257            "params": null,
6258            "_meta": null
6259        }))
6260        .unwrap();
6261        assert_eq!(request_with_null_params.params, None);
6262        assert_eq!(request_with_null_params.meta, None);
6263        for key in ["serverId", "requestId", "method"] {
6264            let mut value =
6265                json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
6266            value.as_object_mut().unwrap().remove(key);
6267            assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
6268        }
6269        for key in ["serverId", "requestId", "method"] {
6270            let mut value =
6271                json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
6272            value[key] = serde_json::Value::Null;
6273            assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
6274        }
6275    }
6276
6277    #[test]
6278    fn test_auth_capabilities_serialize_terminal_support_as_object() {
6279        use serde_json::json;
6280
6281        let capabilities = ClientAuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
6282
6283        assert_eq!(
6284            serde_json::to_value(&capabilities).unwrap(),
6285            json!({
6286                "terminal": {}
6287            })
6288        );
6289
6290        let deserialized: ClientAuthCapabilities = serde_json::from_value(json!({
6291            "terminal": false
6292        }))
6293        .unwrap();
6294        assert!(deserialized.terminal.is_none());
6295    }
6296
6297    #[test]
6298    fn request_permission_request_rejects_malformed_options() {
6299        use serde_json::json;
6300
6301        assert!(
6302            serde_json::from_value::<RequestPermissionRequest>(json!({
6303                "sessionId": "sess-1",
6304                "title": "Run tool?",
6305                "options": "not-an-array"
6306            }))
6307            .is_err()
6308        );
6309        assert!(
6310            serde_json::from_value::<RequestPermissionRequest>(json!({
6311                "sessionId": "sess-1",
6312                "title": "Run tool?",
6313                "options": [{"optionId": "allow"}]
6314            }))
6315            .is_err()
6316        );
6317    }
6318
6319    #[cfg(feature = "unstable_plan_operations")]
6320    #[test]
6321    fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
6322        use serde_json::json;
6323
6324        assert!(
6325            serde_json::from_value::<SessionUpdate>(json!({
6326                "sessionUpdate": "plan_removed"
6327            }))
6328            .is_err()
6329        );
6330    }
6331}