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