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