Skip to main content

agent_client_protocol_schema/v2/
client.rs

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