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, SkipListener};
28
29#[cfg(feature = "unstable_mcp_over_acp")]
30use super::mcp::{
31    ConnectMcpRequest, ConnectMcpResponse, DisconnectMcpRequest, DisconnectMcpResponse,
32    MCP_CONNECT_METHOD_NAME, MCP_DISCONNECT_METHOD_NAME, MCP_MESSAGE_METHOD_NAME,
33    MessageMcpNotification, MessageMcpRequest, MessageMcpResponse,
34};
35
36#[cfg(feature = "unstable_nes")]
37use super::{ClientNesCapabilities, PositionEncodingKind};
38
39// Session updates
40
41/// Notification containing a session update from the agent.
42///
43/// Agents can send session updates at any point while the session exists.
44///
45/// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-lifecycle#3-agent-reports-output)
46#[serde_as]
47#[skip_serializing_none]
48#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION)))]
51#[serde(rename_all = "camelCase")]
52#[non_exhaustive]
53pub struct UpdateSessionNotification {
54    /// The ID of the session this update pertains to.
55    pub session_id: SessionId,
56    /// The actual update content.
57    pub update: SessionUpdate,
58    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
59    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
60    /// these keys.
61    ///
62    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
63    #[serde_as(deserialize_as = "DefaultOnError")]
64    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
65    #[serde(default)]
66    #[serde(rename = "_meta")]
67    pub meta: Option<Meta>,
68}
69
70impl UpdateSessionNotification {
71    /// Builds [`UpdateSessionNotification`] with the required notification fields set; optional fields start unset or empty.
72    #[must_use]
73    pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
74        Self {
75            session_id: session_id.into(),
76            update,
77            meta: None,
78        }
79    }
80
81    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
82    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
83    /// these keys.
84    ///
85    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
86    #[must_use]
87    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
88        self.meta = meta.into_option();
89        self
90    }
91}
92
93/// Different types of updates that can be sent while a session exists.
94///
95/// These updates report messages, progress, and other session activity.
96///
97/// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-lifecycle#3-agent-reports-output)
98#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
100#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
101#[non_exhaustive]
102pub enum SessionUpdate {
103    /// A chunk of the user's message being streamed.
104    UserMessageChunk(ContentChunk),
105    /// A user message has been created or updated.
106    ///
107    /// Agents can send this when they accept or replay a user message. When a
108    /// client receives another `user_message` update with the same `messageId`,
109    /// fields in the new update patch the previous fields for that message.
110    UserMessage(UserMessage),
111    /// A chunk of the agent's response being streamed.
112    AgentMessageChunk(ContentChunk),
113    /// An agent message has been created or updated.
114    ///
115    /// Agents can send this in addition to streamed chunks. When a client
116    /// receives another `agent_message` update with the same `messageId`,
117    /// fields in the new update patch the previous fields for that message.
118    AgentMessage(AgentMessage),
119    /// A chunk of the agent's internal reasoning being streamed.
120    AgentThoughtChunk(ContentChunk),
121    /// An agent thought or reasoning message has been created or updated.
122    ///
123    /// Agents can send this in addition to streamed chunks. When a client
124    /// receives another `agent_thought` update with the same `messageId`,
125    /// fields in the new update patch the previous fields for that message.
126    AgentThought(AgentThought),
127    /// The state of the agent's foreground work has changed.
128    StateUpdate(StateUpdate),
129    /// A chunk of tool-call content being streamed.
130    ToolCallContentChunk(ToolCallContentChunk),
131    /// A tool call has been created or updated.
132    ToolCallUpdate(ToolCallUpdate),
133    /// An agent-owned terminal has been created or updated.
134    TerminalUpdate(TerminalUpdate),
135    /// A chunk of bytes appended to an agent-owned terminal's output.
136    TerminalOutputChunk(TerminalOutputChunk),
137    /// A content update for a plan identified by ID.
138    /// See protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/agent-plan)
139    PlanUpdate(PlanUpdate),
140    /// **UNSTABLE**
141    ///
142    /// This capability is not part of the spec yet, and may be removed or changed at any point.
143    ///
144    /// Removal notice for a plan identified by ID.
145    #[cfg(feature = "unstable_plan_operations")]
146    PlanRemoved(PlanRemoved),
147    /// Available commands are ready or have changed
148    AvailableCommandsUpdate(AvailableCommandsUpdate),
149    /// Session configuration options have been updated.
150    ConfigOptionUpdate(ConfigOptionUpdate),
151    /// Session metadata has been updated (title, timestamps, custom metadata)
152    SessionInfoUpdate(SessionInfoUpdate),
153    /// Context window and cost update for the session.
154    UsageUpdate(UsageUpdate),
155    /// **UNSTABLE**
156    ///
157    /// This capability is not part of the spec yet, and may be removed or changed at any point.
158    ///
159    /// A context compaction has been created or updated.
160    #[cfg(feature = "unstable_session_compaction")]
161    CompactionUpdate(CompactionUpdate),
162    /// **UNSTABLE**
163    ///
164    /// This capability is not part of the spec yet, and may be removed or changed at any point.
165    ///
166    /// A content block appended to a context compaction's retained summary.
167    #[cfg(feature = "unstable_session_compaction")]
168    CompactionSummaryChunk(CompactionSummaryChunk),
169    /// Custom or future session update.
170    ///
171    /// Values beginning with `_` are reserved for implementation-specific
172    /// extensions. Unknown values that do not begin with `_` are reserved for
173    /// future ACP variants.
174    ///
175    /// Receivers that do not understand this update type should preserve the
176    /// raw payload when storing, replaying, proxying, or forwarding session
177    /// history, and otherwise ignore it or display it generically.
178    #[serde(untagged)]
179    Other(OtherSessionUpdate),
180}
181
182/// **UNSTABLE**
183///
184/// This capability is not part of the spec yet, and may be removed or changed at any point.
185///
186/// Unique identifier for a context compaction within a session.
187#[cfg(feature = "unstable_session_compaction")]
188#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
190#[serde(transparent)]
191#[from(forward)]
192#[non_exhaustive]
193pub struct CompactionId(pub Arc<str>);
194
195#[cfg(feature = "unstable_session_compaction")]
196impl CompactionId {
197    /// Wraps a protocol string as a typed [`CompactionId`].
198    #[must_use]
199    pub fn new(id: impl Into<Self>) -> Self {
200        id.into()
201    }
202}
203
204/// **UNSTABLE**
205///
206/// This capability is not part of the spec yet, and may be removed or changed at any point.
207///
208/// Lifecycle state of a context compaction.
209#[cfg(feature = "unstable_session_compaction")]
210#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212#[serde(rename_all = "snake_case")]
213#[non_exhaustive]
214pub enum CompactionStatus {
215    /// Compaction has started and has not finished.
216    InProgress,
217    /// Compaction finished successfully.
218    Completed,
219    /// Compaction finished unsuccessfully.
220    Failed,
221    /// Compaction was cancelled before it finished.
222    Cancelled,
223    /// Custom or future compaction status.
224    ///
225    /// Values beginning with `_` are reserved for implementation-specific
226    /// extensions. Other unknown values are reserved for future ACP statuses.
227    #[serde(untagged)]
228    Other(String),
229}
230
231/// **UNSTABLE**
232///
233/// This capability is not part of the spec yet, and may be removed or changed at any point.
234///
235/// A context compaction upsert. The first update fixes the compaction's
236/// timeline position. Later updates with the same ID patch that entity in place.
237///
238/// `summary`, `error`, and `_meta` have patch semantics: omission leaves the
239/// stored value unchanged, `null` clears it, and a concrete value replaces it.
240/// `summary: []` also clears the retained summary. A non-empty summary is only
241/// valid with `completed`; `error` is only valid with `failed`.
242#[cfg(feature = "unstable_session_compaction")]
243#[serde_as]
244#[skip_serializing_none]
245#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
246#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
247#[serde(rename_all = "camelCase")]
248#[non_exhaustive]
249pub struct CompactionUpdate {
250    /// The Agent-owned ID of this compaction, unique within the session.
251    pub compaction_id: CompactionId,
252    /// Current lifecycle status.
253    pub status: CompactionStatus,
254    /// Complete replacement user-displayable summary retained by the compaction.
255    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
256    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
257    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
258    pub summary: MaybeUndefined<Vec<ContentBlock>>,
259    /// Human-readable description of why the compaction failed.
260    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
261    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
262    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
263    pub error: MaybeUndefined<String>,
264    /// Extensible metadata patch for this compaction.
265    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
266    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
267    #[serde(
268        rename = "_meta",
269        default,
270        skip_serializing_if = "MaybeUndefined::is_undefined"
271    )]
272    pub meta: MaybeUndefined<Meta>,
273}
274
275#[cfg(feature = "unstable_session_compaction")]
276impl CompactionUpdate {
277    /// Builds a compaction update with optional patch fields omitted.
278    #[must_use]
279    pub fn new(compaction_id: impl Into<CompactionId>, status: CompactionStatus) -> Self {
280        Self {
281            compaction_id: compaction_id.into(),
282            status,
283            summary: MaybeUndefined::Undefined,
284            error: MaybeUndefined::Undefined,
285            meta: MaybeUndefined::Undefined,
286        }
287    }
288
289    /// Sets, clears, or omits the complete retained summary patch.
290    #[must_use]
291    pub fn summary(mut self, summary: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
292        self.summary = summary.into_maybe_undefined();
293        self
294    }
295
296    /// Sets, clears, or omits the failure description patch.
297    #[must_use]
298    pub fn error(mut self, error: impl IntoMaybeUndefined<String>) -> Self {
299        self.error = error.into_maybe_undefined();
300        self
301    }
302
303    /// Sets, clears, or omits the metadata patch.
304    #[must_use]
305    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
306        self.meta = meta.into_maybe_undefined();
307        self
308    }
309}
310
311/// **UNSTABLE**
312///
313/// This capability is not part of the spec yet, and may be removed or changed at any point.
314///
315/// A content block appended to the retained summary of an in-progress
316/// compaction. Agents send chunks only after an `in_progress` update and before
317/// the terminal update for the same ID.
318#[cfg(feature = "unstable_session_compaction")]
319#[serde_as]
320#[skip_serializing_none]
321#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
323#[serde(rename_all = "camelCase")]
324#[non_exhaustive]
325pub struct CompactionSummaryChunk {
326    /// ID of the compaction whose summary receives this content.
327    pub compaction_id: CompactionId,
328    /// One content block to append.
329    pub content: ContentBlock,
330    /// Metadata scoped to this chunk. Omission and `null` both mean absent.
331    #[serde_as(deserialize_as = "DefaultOnError")]
332    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
333    #[serde(default, rename = "_meta")]
334    pub meta: Option<Meta>,
335}
336
337#[cfg(feature = "unstable_session_compaction")]
338impl CompactionSummaryChunk {
339    /// Builds a summary chunk without metadata.
340    #[must_use]
341    pub fn new(compaction_id: impl Into<CompactionId>, content: ContentBlock) -> Self {
342        Self {
343            compaction_id: compaction_id.into(),
344            content,
345            meta: None,
346        }
347    }
348
349    /// Sets or clears chunk-scoped metadata.
350    #[must_use]
351    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
352        self.meta = meta.into_option();
353        self
354    }
355}
356
357/// Custom or future session update payload.
358///
359/// This preserves the unknown `sessionUpdate` discriminator and the rest of the
360/// update object for clients that store, replay, proxy, or forward session
361/// history.
362#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
363#[derive(Debug, Clone, Serialize, PartialEq)]
364#[cfg_attr(feature = "schemars", schemars(inline))]
365#[cfg_attr(feature = "schemars", schemars(transform = other_session_update_schema))]
366#[serde(rename_all = "camelCase")]
367#[non_exhaustive]
368pub struct OtherSessionUpdate {
369    /// Custom or future session update type.
370    ///
371    /// Values beginning with `_` are reserved for implementation-specific
372    /// extensions. Unknown values that do not begin with `_` are reserved for
373    /// future ACP variants.
374    #[serde(rename = "sessionUpdate")]
375    pub session_update: String,
376    /// Additional fields from the unknown update payload.
377    #[serde(flatten)]
378    pub fields: BTreeMap<String, serde_json::Value>,
379}
380
381impl OtherSessionUpdate {
382    /// Builds [`OtherSessionUpdate`] from an unknown discriminator and preserves the remaining extension fields.
383    #[must_use]
384    pub fn new(
385        session_update: impl Into<String>,
386        mut fields: BTreeMap<String, serde_json::Value>,
387    ) -> Self {
388        fields.remove("sessionUpdate");
389        Self {
390            session_update: session_update.into(),
391            fields,
392        }
393    }
394}
395
396impl<'de> Deserialize<'de> for OtherSessionUpdate {
397    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398    where
399        D: serde::Deserializer<'de>,
400    {
401        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
402        let session_update = fields
403            .remove("sessionUpdate")
404            .ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
405        let serde_json::Value::String(session_update) = session_update else {
406            return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
407        };
408
409        if is_known_session_update(&session_update) {
410            return Err(serde::de::Error::custom(format!(
411                "known session update `{session_update}` did not match its schema"
412            )));
413        }
414
415        Ok(Self {
416            session_update,
417            fields,
418        })
419    }
420}
421
422fn is_known_session_update(session_update: &str) -> bool {
423    #[cfg(feature = "unstable_session_compaction")]
424    if matches!(
425        session_update,
426        "compaction_update" | "compaction_summary_chunk"
427    ) {
428        return true;
429    }
430    #[cfg(feature = "unstable_plan_operations")]
431    if session_update == "plan_removed" {
432        return true;
433    }
434    matches!(
435        session_update,
436        "user_message_chunk"
437            | "user_message"
438            | "agent_message_chunk"
439            | "agent_message"
440            | "agent_thought_chunk"
441            | "agent_thought"
442            | "state_update"
443            | "tool_call_content_chunk"
444            | "tool_call_update"
445            | "terminal_update"
446            | "terminal_output_chunk"
447            | "plan_update"
448            | "available_commands_update"
449            | "config_option_update"
450            | "session_info_update"
451            | "usage_update"
452    )
453}
454
455#[cfg(feature = "schemars")]
456fn other_session_update_schema(schema: &mut Schema) {
457    super::schema_util::reject_known_string_discriminators(
458        schema,
459        "sessionUpdate",
460        &[
461            "user_message_chunk",
462            "user_message",
463            "agent_message_chunk",
464            "agent_message",
465            "agent_thought_chunk",
466            "agent_thought",
467            "state_update",
468            "tool_call_content_chunk",
469            "tool_call_update",
470            "terminal_update",
471            "terminal_output_chunk",
472            "plan_update",
473            "available_commands_update",
474            "config_option_update",
475            "session_info_update",
476            #[cfg(feature = "unstable_plan_operations")]
477            "plan_removed",
478            "usage_update",
479            #[cfg(feature = "unstable_session_compaction")]
480            "compaction_update",
481            #[cfg(feature = "unstable_session_compaction")]
482            "compaction_summary_chunk",
483        ],
484    );
485}
486
487/// Session configuration options have been updated.
488#[serde_as]
489#[skip_serializing_none]
490#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
491#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
492#[serde(rename_all = "camelCase")]
493#[non_exhaustive]
494pub struct ConfigOptionUpdate {
495    /// The full set of configuration options and their current values.
496    #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
497    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
498    pub config_options: Vec<SessionConfigOption>,
499    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
500    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
501    /// these keys.
502    ///
503    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
504    #[serde_as(deserialize_as = "DefaultOnError")]
505    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
506    #[serde(default)]
507    #[serde(rename = "_meta")]
508    pub meta: Option<Meta>,
509}
510
511impl ConfigOptionUpdate {
512    /// Builds [`ConfigOptionUpdate`] with the required fields set; optional fields start unset or empty.
513    #[must_use]
514    pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
515        Self {
516            config_options,
517            meta: None,
518        }
519    }
520
521    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
522    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
523    /// these keys.
524    ///
525    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
526    #[must_use]
527    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
528        self.meta = meta.into_option();
529        self
530    }
531}
532
533/// Update to session metadata. All fields are optional to support partial updates.
534///
535/// Agents send this notification to update session information like title or custom metadata.
536/// This allows clients to display dynamic session names and track session state changes.
537///
538/// Omitted fields leave the existing session info unchanged. `null` clears the
539/// corresponding value.
540#[serde_as]
541#[skip_serializing_none]
542#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
543#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
544#[serde(rename_all = "camelCase")]
545#[non_exhaustive]
546pub struct SessionInfoUpdate {
547    /// Human-readable title for the session. Set to null to clear.
548    #[serde_as(deserialize_as = "DefaultOnError")]
549    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
550    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
551    pub title: MaybeUndefined<String>,
552    /// RFC 3339 timestamp of last activity. Set to null to clear.
553    #[serde_as(deserialize_as = "DefaultOnError")]
554    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
555    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
556    pub updated_at: MaybeUndefined<String>,
557    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
558    /// metadata to their interactions. Omitted means no metadata update; `null` is an
559    /// explicit clear signal. Implementations MUST NOT make assumptions about values at these keys.
560    ///
561    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
562    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
563    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
564    #[serde(
565        rename = "_meta",
566        default,
567        skip_serializing_if = "MaybeUndefined::is_undefined"
568    )]
569    pub meta: MaybeUndefined<Meta>,
570}
571
572impl SessionInfoUpdate {
573    /// Builds [`SessionInfoUpdate`] with the required fields set; optional fields start unset or empty.
574    #[must_use]
575    pub fn new() -> Self {
576        Self::default()
577    }
578
579    /// Human-readable title for the session. Set to null to clear.
580    #[must_use]
581    pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
582        self.title = title.into_maybe_undefined();
583        self
584    }
585
586    /// RFC 3339 timestamp of last activity. Set to null to clear.
587    #[must_use]
588    pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
589        self.updated_at = updated_at.into_maybe_undefined();
590        self
591    }
592
593    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
594    /// metadata to their interactions. Omitted means no metadata update; `null` is an
595    /// explicit clear signal. Implementations MUST NOT make assumptions about values at these keys.
596    ///
597    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
598    #[must_use]
599    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
600        self.meta = meta.into_maybe_undefined();
601        self
602    }
603}
604
605/// Context window and cost update for a session.
606#[serde_as]
607#[skip_serializing_none]
608#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
609#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
610#[serde(rename_all = "camelCase")]
611#[non_exhaustive]
612pub struct UsageUpdate {
613    /// Tokens currently in context.
614    pub used: u64,
615    /// Total context window size in tokens.
616    pub size: u64,
617    /// Cumulative session cost (optional).
618    #[serde_as(deserialize_as = "DefaultOnError")]
619    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
620    #[serde(default)]
621    pub cost: Option<Cost>,
622    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
623    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
624    /// these keys.
625    ///
626    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
627    #[serde_as(deserialize_as = "DefaultOnError")]
628    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
629    #[serde(default)]
630    #[serde(rename = "_meta")]
631    pub meta: Option<Meta>,
632}
633
634impl UsageUpdate {
635    /// Builds [`UsageUpdate`] with the required fields set; optional fields start unset or empty.
636    #[must_use]
637    pub fn new(used: u64, size: u64) -> Self {
638        Self {
639            used,
640            size,
641            cost: None,
642            meta: None,
643        }
644    }
645
646    /// Cumulative session cost (optional).
647    #[must_use]
648    pub fn cost(mut self, cost: impl IntoOption<Cost>) -> Self {
649        self.cost = cost.into_option();
650        self
651    }
652
653    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
654    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
655    /// these keys.
656    ///
657    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
658    #[must_use]
659    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
660        self.meta = meta.into_option();
661        self
662    }
663}
664
665/// The state of the agent's foreground work has changed.
666///
667/// Background activity can continue and emit other `session/update` notifications
668/// while `idle`. Those notifications do not change this state.
669#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
671#[serde(tag = "state", rename_all = "snake_case")]
672#[non_exhaustive]
673pub enum StateUpdate {
674    /// Foreground work is in progress.
675    Running(RunningStateUpdate),
676    /// The agent is ready to process a new prompt.
677    Idle(IdleStateUpdate),
678    /// Foreground work is blocked on user action.
679    RequiresAction(RequiresActionStateUpdate),
680    /// Custom or future session state.
681    ///
682    /// Values beginning with `_` are reserved for implementation-specific
683    /// extensions. Unknown values that do not begin with `_` are reserved for
684    /// future ACP variants.
685    #[serde(untagged)]
686    Other(OtherStateUpdate),
687}
688
689/// Foreground work is in progress.
690#[serde_as]
691#[skip_serializing_none]
692#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
693#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
694#[serde(rename_all = "camelCase")]
695#[non_exhaustive]
696pub struct RunningStateUpdate {
697    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
698    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
699    /// these keys.
700    ///
701    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
702    #[serde_as(deserialize_as = "DefaultOnError")]
703    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
704    #[serde(default)]
705    #[serde(rename = "_meta")]
706    pub meta: Option<Meta>,
707}
708
709impl RunningStateUpdate {
710    /// Builds [`RunningStateUpdate`] with the required fields set; optional fields start unset or empty.
711    #[must_use]
712    pub fn new() -> Self {
713        Self::default()
714    }
715
716    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
717    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
718    /// these keys.
719    ///
720    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
721    #[must_use]
722    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
723        self.meta = meta.into_option();
724        self
725    }
726}
727
728/// The agent is ready to process a new prompt.
729#[serde_as]
730#[skip_serializing_none]
731#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
732#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
733#[serde(rename_all = "camelCase")]
734#[non_exhaustive]
735pub struct IdleStateUpdate {
736    /// Indicates why foreground work stopped.
737    ///
738    /// Optional. Omitted or `null` both mean the agent is not reporting a stop reason.
739    /// Agents SHOULD include this when the idle transition ends foreground work.
740    #[serde_as(deserialize_as = "DefaultOnError")]
741    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
742    #[serde(default)]
743    pub stop_reason: Option<StopReason>,
744    /// **UNSTABLE**
745    ///
746    /// This capability is not part of the spec yet, and may be removed or changed at any point.
747    ///
748    /// Token usage for completed foreground work.
749    ///
750    /// Optional. Omitted or `null` both mean the agent is not reporting token
751    /// usage for this state update.
752    #[cfg(feature = "unstable_end_turn_token_usage")]
753    #[serde_as(deserialize_as = "DefaultOnError")]
754    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
755    #[serde(default)]
756    pub usage: Option<Usage>,
757    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
758    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
759    /// these keys.
760    ///
761    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
762    #[serde_as(deserialize_as = "DefaultOnError")]
763    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
764    #[serde(default)]
765    #[serde(rename = "_meta")]
766    pub meta: Option<Meta>,
767}
768
769impl IdleStateUpdate {
770    /// Builds [`IdleStateUpdate`] with the required fields set; optional fields start unset or empty.
771    #[must_use]
772    pub fn new() -> Self {
773        Self::default()
774    }
775
776    /// Indicates why foreground work stopped.
777    #[must_use]
778    pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
779        self.stop_reason = stop_reason.into_option();
780        self
781    }
782
783    /// **UNSTABLE**
784    ///
785    /// This capability is not part of the spec yet, and may be removed or changed at any point.
786    ///
787    /// Token usage for completed foreground work.
788    #[cfg(feature = "unstable_end_turn_token_usage")]
789    #[must_use]
790    pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
791        self.usage = usage.into_option();
792        self
793    }
794
795    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
796    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
797    /// these keys.
798    ///
799    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
800    #[must_use]
801    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
802        self.meta = meta.into_option();
803        self
804    }
805}
806
807/// Foreground work is blocked on user action.
808#[serde_as]
809#[skip_serializing_none]
810#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
811#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
812#[serde(rename_all = "camelCase")]
813#[non_exhaustive]
814pub struct RequiresActionStateUpdate {
815    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
816    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
817    /// these keys.
818    ///
819    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
820    #[serde_as(deserialize_as = "DefaultOnError")]
821    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
822    #[serde(default)]
823    #[serde(rename = "_meta")]
824    pub meta: Option<Meta>,
825}
826
827impl RequiresActionStateUpdate {
828    /// Builds [`RequiresActionStateUpdate`] with the required fields set; optional fields start unset or empty.
829    #[must_use]
830    pub fn new() -> Self {
831        Self::default()
832    }
833
834    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
835    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
836    /// these keys.
837    ///
838    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
839    #[must_use]
840    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
841        self.meta = meta.into_option();
842        self
843    }
844}
845
846/// Custom or future session state payload.
847///
848/// This preserves the unknown `state` discriminator and the rest of the state
849/// object for clients that store, replay, proxy, or forward session history.
850#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
851#[derive(Debug, Clone, Serialize, PartialEq)]
852#[cfg_attr(feature = "schemars", schemars(inline))]
853#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
854#[serde(rename_all = "camelCase")]
855#[non_exhaustive]
856pub struct OtherStateUpdate {
857    /// Custom or future session state.
858    ///
859    /// Values beginning with `_` are reserved for implementation-specific
860    /// extensions. Unknown values that do not begin with `_` are reserved for
861    /// future ACP variants.
862    #[serde(rename = "state")]
863    pub state: String,
864    /// Additional fields from the unknown state payload.
865    #[serde(flatten)]
866    pub fields: BTreeMap<String, serde_json::Value>,
867}
868
869impl OtherStateUpdate {
870    /// Builds [`OtherStateUpdate`] from an unknown discriminator and preserves the remaining extension fields.
871    #[must_use]
872    pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
873        fields.remove("state");
874        Self {
875            state: state.into(),
876            fields,
877        }
878    }
879}
880
881impl<'de> Deserialize<'de> for OtherStateUpdate {
882    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
883    where
884        D: serde::Deserializer<'de>,
885    {
886        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
887        let state = fields
888            .remove("state")
889            .ok_or_else(|| serde::de::Error::missing_field("state"))?;
890        let serde_json::Value::String(state) = state else {
891            return Err(serde::de::Error::custom("`state` must be a string"));
892        };
893
894        if is_known_state_update(&state) {
895            return Err(serde::de::Error::custom(format!(
896                "known state update `{state}` did not match its schema"
897            )));
898        }
899
900        Ok(Self { state, fields })
901    }
902}
903
904fn is_known_state_update(state: &str) -> bool {
905    matches!(state, "running" | "idle" | "requires_action")
906}
907
908#[cfg(feature = "schemars")]
909fn other_state_update_schema(schema: &mut Schema) {
910    super::schema_util::reject_known_string_discriminators(
911        schema,
912        "state",
913        &["running", "idle", "requires_action"],
914    );
915}
916
917/// Cost information for a session.
918#[serde_as]
919#[skip_serializing_none]
920#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
921#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
922#[serde(rename_all = "camelCase")]
923#[non_exhaustive]
924pub struct Cost {
925    /// Total cumulative cost for session.
926    pub amount: f64,
927    /// ISO 4217 currency code (e.g., "USD", "EUR").
928    #[cfg_attr(feature = "schemars", schemars(pattern(r"^[A-Z]{3}$")))]
929    pub currency: String,
930    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
931    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
932    /// these keys.
933    ///
934    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
935    #[serde_as(deserialize_as = "DefaultOnError")]
936    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
937    #[serde(default)]
938    #[serde(rename = "_meta")]
939    pub meta: Option<Meta>,
940}
941
942impl Cost {
943    /// Builds [`Cost`] with the required fields set; optional fields start unset or empty.
944    #[must_use]
945    pub fn new(amount: f64, currency: impl Into<String>) -> Self {
946        Self {
947            amount,
948            currency: currency.into(),
949            meta: None,
950        }
951    }
952
953    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
954    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
955    /// these keys.
956    ///
957    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
958    #[must_use]
959    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
960        self.meta = meta.into_option();
961        self
962    }
963}
964
965/// A streamed item of message content.
966#[serde_as]
967#[skip_serializing_none]
968#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
969#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
970#[serde(rename_all = "camelCase")]
971#[non_exhaustive]
972pub struct ContentChunk {
973    /// A unique identifier for the message this chunk belongs to.
974    ///
975    /// All chunks belonging to the same message share the same `messageId`.
976    /// A change in `messageId` indicates a new message has started.
977    pub message_id: MessageId,
978    /// A single item of content
979    pub content: ContentBlock,
980    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
981    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
982    /// these keys. This field is chunk-scoped.
983    ///
984    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
985    #[serde_as(deserialize_as = "DefaultOnError")]
986    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
987    #[serde(default)]
988    #[serde(rename = "_meta")]
989    pub meta: Option<Meta>,
990}
991
992impl ContentChunk {
993    /// Builds [`ContentChunk`] with the required fields set; optional fields start unset or empty.
994    #[must_use]
995    pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
996        Self {
997            content,
998            message_id: message_id.into(),
999            meta: None,
1000        }
1001    }
1002
1003    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1004    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1005    /// these keys. This field is chunk-scoped.
1006    ///
1007    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1008    #[must_use]
1009    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1010        self.meta = meta.into_option();
1011        self
1012    }
1013}
1014
1015/// A user message upsert.
1016///
1017/// Only [`UserMessage::message_id`] is required. `content` has patch semantics:
1018/// an omitted field leaves existing message content unchanged, `null` clears the
1019/// value, and a concrete array replaces the previous value. For a new
1020/// `messageId`, omitted fields use client defaults. `content` is replaced as a
1021/// whole array; send `[]` or `null` to clear it.
1022///
1023/// Message updates and chunks are applied in the order they are received. When
1024/// a `user_message` update includes `content`, that array replaces any content
1025/// previously accumulated for the message, including content from earlier
1026/// chunks. Later chunks with the same `messageId` append to the current
1027/// content.
1028#[serde_as]
1029#[skip_serializing_none]
1030#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1031#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1032#[serde(rename_all = "camelCase")]
1033#[non_exhaustive]
1034pub struct UserMessage {
1035    /// A unique identifier for the message.
1036    pub message_id: MessageId,
1037    /// Complete replacement content for this message.
1038    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
1039    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1040    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1041    pub content: MaybeUndefined<Vec<ContentBlock>>,
1042    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1043    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1044    /// these keys. Omitted means no metadata update; `null` is an explicit clear signal.
1045    ///
1046    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1047    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1048    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1049    #[serde(
1050        rename = "_meta",
1051        default,
1052        skip_serializing_if = "MaybeUndefined::is_undefined"
1053    )]
1054    pub meta: MaybeUndefined<Meta>,
1055}
1056
1057impl UserMessage {
1058    /// Builds [`UserMessage`] with the required fields set; optional fields start unset or empty.
1059    #[must_use]
1060    pub fn new(message_id: impl Into<MessageId>) -> Self {
1061        Self {
1062            message_id: message_id.into(),
1063            content: MaybeUndefined::Undefined,
1064            meta: MaybeUndefined::Undefined,
1065        }
1066    }
1067
1068    /// Complete replacement content for this message.
1069    #[must_use]
1070    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
1071        self.content = content.into_maybe_undefined();
1072        self
1073    }
1074
1075    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1076    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1077    /// these keys.
1078    ///
1079    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1080    #[must_use]
1081    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1082        self.meta = meta.into_maybe_undefined();
1083        self
1084    }
1085}
1086
1087/// An agent message upsert.
1088///
1089/// Only [`AgentMessage::message_id`] is required. `content` has patch semantics:
1090/// an omitted field leaves existing message content unchanged, `null` clears the
1091/// value, and a concrete array replaces the previous value. For a new
1092/// `messageId`, omitted fields use client defaults. `content` is replaced as a
1093/// whole array; send `[]` or `null` to clear it.
1094///
1095/// Message updates and chunks are applied in the order they are received. When
1096/// an `agent_message` update includes `content`, that array replaces any
1097/// content previously accumulated for the message, including content from
1098/// earlier chunks. Later chunks with the same `messageId` append to the current
1099/// content.
1100#[serde_as]
1101#[skip_serializing_none]
1102#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1104#[serde(rename_all = "camelCase")]
1105#[non_exhaustive]
1106pub struct AgentMessage {
1107    /// A unique identifier for the message.
1108    pub message_id: MessageId,
1109    /// Complete replacement content for this message.
1110    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
1111    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1112    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1113    pub content: MaybeUndefined<Vec<ContentBlock>>,
1114    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1115    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1116    /// these keys. Omitted means no metadata update; `null` is an explicit clear signal.
1117    ///
1118    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1119    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1120    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1121    #[serde(
1122        rename = "_meta",
1123        default,
1124        skip_serializing_if = "MaybeUndefined::is_undefined"
1125    )]
1126    pub meta: MaybeUndefined<Meta>,
1127}
1128
1129impl AgentMessage {
1130    /// Builds [`AgentMessage`] with the required fields set; optional fields start unset or empty.
1131    #[must_use]
1132    pub fn new(message_id: impl Into<MessageId>) -> Self {
1133        Self {
1134            message_id: message_id.into(),
1135            content: MaybeUndefined::Undefined,
1136            meta: MaybeUndefined::Undefined,
1137        }
1138    }
1139
1140    /// Complete replacement content for this message.
1141    #[must_use]
1142    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
1143        self.content = content.into_maybe_undefined();
1144        self
1145    }
1146
1147    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1148    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1149    /// these keys.
1150    ///
1151    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1152    #[must_use]
1153    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1154        self.meta = meta.into_maybe_undefined();
1155        self
1156    }
1157}
1158
1159/// An agent thought or reasoning message upsert.
1160///
1161/// Only [`AgentThought::message_id`] is required. `content` has patch semantics:
1162/// an omitted field leaves existing thought content unchanged, `null` clears the
1163/// value, and a concrete array replaces the previous value. For a new
1164/// `messageId`, omitted fields use client defaults. `content` is replaced as a
1165/// whole array; send `[]` or `null` to clear it.
1166///
1167/// Message updates and chunks are applied in the order they are received. When
1168/// an `agent_thought` update includes `content`, that array replaces any
1169/// content previously accumulated for the thought, including content from
1170/// earlier chunks. Later chunks with the same `messageId` append to the current
1171/// content.
1172#[serde_as]
1173#[skip_serializing_none]
1174#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1176#[serde(rename_all = "camelCase")]
1177#[non_exhaustive]
1178pub struct AgentThought {
1179    /// A unique identifier for the thought message.
1180    pub message_id: MessageId,
1181    /// Complete replacement content for this thought message.
1182    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
1183    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1184    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1185    pub content: MaybeUndefined<Vec<ContentBlock>>,
1186    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1187    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1188    /// these keys. Omitted means no metadata update; `null` is an explicit clear signal.
1189    ///
1190    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1191    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1192    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1193    #[serde(
1194        rename = "_meta",
1195        default,
1196        skip_serializing_if = "MaybeUndefined::is_undefined"
1197    )]
1198    pub meta: MaybeUndefined<Meta>,
1199}
1200
1201impl AgentThought {
1202    /// Builds [`AgentThought`] with the required fields set; optional fields start unset or empty.
1203    #[must_use]
1204    pub fn new(message_id: impl Into<MessageId>) -> Self {
1205        Self {
1206            message_id: message_id.into(),
1207            content: MaybeUndefined::Undefined,
1208            meta: MaybeUndefined::Undefined,
1209        }
1210    }
1211
1212    /// Complete replacement content for this thought message.
1213    #[must_use]
1214    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
1215        self.content = content.into_maybe_undefined();
1216        self
1217    }
1218
1219    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1220    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1221    /// these keys.
1222    ///
1223    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1224    #[must_use]
1225    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1226        self.meta = meta.into_maybe_undefined();
1227        self
1228    }
1229}
1230
1231/// Unique identifier for a message within a session.
1232#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
1234#[serde(transparent)]
1235#[from(forward)]
1236#[non_exhaustive]
1237pub struct MessageId(pub Arc<str>);
1238
1239impl MessageId {
1240    /// Wraps a protocol string as a typed [`MessageId`].
1241    #[must_use]
1242    pub fn new(id: impl Into<Self>) -> Self {
1243        id.into()
1244    }
1245}
1246
1247/// Available commands are ready or have changed
1248#[serde_as]
1249#[skip_serializing_none]
1250#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1251#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1252#[serde(rename_all = "camelCase")]
1253#[non_exhaustive]
1254pub struct AvailableCommandsUpdate {
1255    /// Commands the agent can execute.
1256    #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
1257    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1258    pub available_commands: Vec<AvailableCommand>,
1259    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1260    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1261    /// these keys.
1262    ///
1263    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1264    #[serde_as(deserialize_as = "DefaultOnError")]
1265    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1266    #[serde(default)]
1267    #[serde(rename = "_meta")]
1268    pub meta: Option<Meta>,
1269}
1270
1271impl AvailableCommandsUpdate {
1272    /// Builds [`AvailableCommandsUpdate`] with the required fields set; optional fields start unset or empty.
1273    #[must_use]
1274    pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
1275        Self {
1276            available_commands,
1277            meta: None,
1278        }
1279    }
1280
1281    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1282    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1283    /// these keys.
1284    ///
1285    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1286    #[must_use]
1287    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1288        self.meta = meta.into_option();
1289        self
1290    }
1291}
1292
1293/// Information about a command.
1294#[serde_as]
1295#[skip_serializing_none]
1296#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1298#[serde(rename_all = "camelCase")]
1299#[non_exhaustive]
1300pub struct AvailableCommand {
1301    /// Command name (e.g., `create_plan`, `research_codebase`).
1302    pub name: String,
1303    /// Human-readable description of what the command does.
1304    pub description: String,
1305    /// Input for the command if required
1306    #[serde_as(deserialize_as = "DefaultOnError")]
1307    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1308    #[serde(default)]
1309    pub input: Option<AvailableCommandInput>,
1310    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1311    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1312    /// these keys.
1313    ///
1314    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1315    #[serde_as(deserialize_as = "DefaultOnError")]
1316    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1317    #[serde(default)]
1318    #[serde(rename = "_meta")]
1319    pub meta: Option<Meta>,
1320}
1321
1322impl AvailableCommand {
1323    /// Builds [`AvailableCommand`] with the required fields set; optional fields start unset or empty.
1324    #[must_use]
1325    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
1326        Self {
1327            name: name.into(),
1328            description: description.into(),
1329            input: None,
1330            meta: None,
1331        }
1332    }
1333
1334    /// Input for the command if required
1335    #[must_use]
1336    pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
1337        self.input = input.into_option();
1338        self
1339    }
1340
1341    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1342    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1343    /// these keys.
1344    ///
1345    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1346    #[must_use]
1347    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1348        self.meta = meta.into_option();
1349        self
1350    }
1351}
1352
1353/// The input specification for a command.
1354#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1356#[serde(tag = "type", rename_all = "snake_case")]
1357#[non_exhaustive]
1358pub enum AvailableCommandInput {
1359    /// All text that was typed after the command name is provided as input.
1360    #[serde(rename = "text")]
1361    Text(TextCommandInput),
1362    /// Custom or future command input specification.
1363    ///
1364    /// Values beginning with `_` are reserved for implementation-specific
1365    /// extensions. Unknown values that do not begin with `_` are reserved for
1366    /// future ACP variants.
1367    ///
1368    /// Clients that do not understand this input type should preserve the raw
1369    /// payload when storing, replaying, proxying, or forwarding command
1370    /// metadata, and otherwise ignore the input specification or display the
1371    /// command without structured input.
1372    #[serde(untagged)]
1373    Other(OtherAvailableCommandInput),
1374}
1375
1376/// Custom or future command input specification.
1377#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1378#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1379#[cfg_attr(feature = "schemars", schemars(inline))]
1380#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
1381#[serde(rename_all = "camelCase")]
1382#[non_exhaustive]
1383pub struct OtherAvailableCommandInput {
1384    /// Custom or future command input type.
1385    ///
1386    /// Values beginning with `_` are reserved for implementation-specific
1387    /// extensions. Unknown values that do not begin with `_` are reserved for
1388    /// future ACP variants.
1389    #[serde(rename = "type")]
1390    pub type_: String,
1391    /// Additional fields from the unknown command input payload.
1392    #[serde(flatten)]
1393    pub fields: BTreeMap<String, serde_json::Value>,
1394}
1395
1396impl OtherAvailableCommandInput {
1397    /// Builds [`OtherAvailableCommandInput`] from an unknown discriminator and preserves the remaining extension fields.
1398    #[must_use]
1399    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1400        fields.remove("type");
1401        Self {
1402            type_: type_.into(),
1403            fields,
1404        }
1405    }
1406}
1407
1408impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
1409    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1410    where
1411        D: serde::Deserializer<'de>,
1412    {
1413        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1414        let type_ = fields
1415            .remove("type")
1416            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
1417        let serde_json::Value::String(type_) = type_ else {
1418            return Err(serde::de::Error::custom("`type` must be a string"));
1419        };
1420
1421        if is_known_available_command_input_type(&type_) {
1422            return Err(serde::de::Error::custom(format!(
1423                "known available command input type `{type_}` did not match its schema"
1424            )));
1425        }
1426
1427        Ok(Self { type_, fields })
1428    }
1429}
1430
1431const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
1432
1433fn is_known_available_command_input_type(type_: &str) -> bool {
1434    KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
1435}
1436
1437#[cfg(feature = "schemars")]
1438fn other_available_command_input_schema(schema: &mut Schema) {
1439    super::schema_util::reject_known_string_discriminators(
1440        schema,
1441        "type",
1442        KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
1443    );
1444}
1445
1446/// All text that was typed after the command name is provided as input.
1447#[serde_as]
1448#[skip_serializing_none]
1449#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1451#[serde(rename_all = "camelCase")]
1452#[non_exhaustive]
1453pub struct TextCommandInput {
1454    /// A hint to display when the input hasn't been provided yet
1455    pub hint: String,
1456    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1457    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1458    /// these keys.
1459    ///
1460    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1461    #[serde_as(deserialize_as = "DefaultOnError")]
1462    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1463    #[serde(default)]
1464    #[serde(rename = "_meta")]
1465    pub meta: Option<Meta>,
1466}
1467
1468impl TextCommandInput {
1469    /// Builds [`TextCommandInput`] with the required fields set; optional fields start unset or empty.
1470    #[must_use]
1471    pub fn new(hint: impl Into<String>) -> Self {
1472        Self {
1473            hint: hint.into(),
1474            meta: None,
1475        }
1476    }
1477
1478    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1479    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1480    /// these keys.
1481    ///
1482    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1483    #[must_use]
1484    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1485        self.meta = meta.into_option();
1486        self
1487    }
1488}
1489
1490// Permission
1491
1492/// Request for user permission to proceed with an operation.
1493///
1494/// Sent when the agent needs authorization before performing a sensitive operation.
1495///
1496/// See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission)
1497#[serde_as]
1498#[skip_serializing_none]
1499#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1501#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
1502#[serde(rename_all = "camelCase")]
1503#[non_exhaustive]
1504pub struct RequestPermissionRequest {
1505    /// The session ID for this request.
1506    pub session_id: SessionId,
1507    /// Human-readable title for the permission prompt.
1508    ///
1509    /// This title is specific to the permission prompt and does not update any
1510    /// subject's displayed title.
1511    pub title: String,
1512    /// Optional human-readable explanation of why permission is needed.
1513    ///
1514    /// This text is specific to the permission prompt and does not update any
1515    /// subject's displayed content. Omitted or `null` both mean no separate
1516    /// permission description was provided.
1517    #[serde_as(deserialize_as = "DefaultOnError")]
1518    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1519    #[serde(default)]
1520    pub description: Option<String>,
1521    /// Optional structured context about the operation requiring permission.
1522    ///
1523    /// Omitted or `null` both mean no structured subject was provided.
1524    #[serde(default)]
1525    pub subject: Option<RequestPermissionSubject>,
1526    /// Available permission options for the user to choose from.
1527    /// Must contain at least one option.
1528    #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
1529    pub options: Vec<PermissionOption>,
1530    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1531    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1532    /// these keys.
1533    ///
1534    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1535    #[serde_as(deserialize_as = "DefaultOnError")]
1536    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1537    #[serde(default)]
1538    #[serde(rename = "_meta")]
1539    pub meta: Option<Meta>,
1540}
1541
1542impl RequestPermissionRequest {
1543    /// Builds [`RequestPermissionRequest`] with the required request fields set; optional fields start unset or empty.
1544    #[must_use]
1545    pub fn new(
1546        session_id: impl Into<SessionId>,
1547        title: impl Into<String>,
1548        options: Vec<PermissionOption>,
1549    ) -> Self {
1550        Self {
1551            session_id: session_id.into(),
1552            title: title.into(),
1553            description: None,
1554            subject: None,
1555            options,
1556            meta: None,
1557        }
1558    }
1559
1560    /// Sets or clears the optional `description` field.
1561    #[must_use]
1562    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
1563        self.description = description.into_option();
1564        self
1565    }
1566
1567    /// Sets or clears the optional `subject` field.
1568    #[must_use]
1569    pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
1570        self.subject = subject.into_option();
1571        self
1572    }
1573
1574    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1575    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1576    /// these keys.
1577    ///
1578    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1579    #[must_use]
1580    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1581        self.meta = meta.into_option();
1582        self
1583    }
1584}
1585
1586/// The operation requiring permission.
1587#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1589#[serde(tag = "type", rename_all = "snake_case")]
1590#[non_exhaustive]
1591pub enum RequestPermissionSubject {
1592    /// Permission is requested before executing a tool call.
1593    ToolCall(Box<ToolCallPermissionSubject>),
1594    /// Permission is requested before running a command.
1595    Command(CommandPermissionSubject),
1596    /// Custom or future permission subject.
1597    ///
1598    /// Values beginning with `_` are reserved for implementation-specific
1599    /// extensions. Unknown values that do not begin with `_` are reserved for
1600    /// future ACP variants.
1601    ///
1602    /// Clients that do not understand this subject type should preserve the raw
1603    /// payload when storing, replaying, proxying, or forwarding permission
1604    /// requests, and otherwise display a generic permission prompt or decline it
1605    /// according to policy.
1606    #[serde(untagged)]
1607    Other(OtherRequestPermissionSubject),
1608}
1609
1610impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
1611    fn from(subject: ToolCallPermissionSubject) -> Self {
1612        Self::ToolCall(Box::new(subject))
1613    }
1614}
1615
1616impl From<ToolCallUpdate> for RequestPermissionSubject {
1617    fn from(tool_call: ToolCallUpdate) -> Self {
1618        ToolCallPermissionSubject::new(tool_call).into()
1619    }
1620}
1621
1622impl From<CommandPermissionSubject> for RequestPermissionSubject {
1623    fn from(subject: CommandPermissionSubject) -> Self {
1624        Self::Command(subject)
1625    }
1626}
1627
1628/// Permission request details for a tool call.
1629#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1630#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1631#[serde(rename_all = "camelCase")]
1632#[non_exhaustive]
1633pub struct ToolCallPermissionSubject {
1634    /// Details about the tool call requiring permission.
1635    pub tool_call: ToolCallUpdate,
1636}
1637
1638impl ToolCallPermissionSubject {
1639    /// Builds [`ToolCallPermissionSubject`] with the required fields set.
1640    #[must_use]
1641    pub fn new(tool_call: ToolCallUpdate) -> Self {
1642        Self { tool_call }
1643    }
1644}
1645
1646/// Permission request details for a command.
1647#[serde_as]
1648#[skip_serializing_none]
1649#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1650#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1651#[serde(rename_all = "camelCase")]
1652#[non_exhaustive]
1653pub struct CommandPermissionSubject {
1654    /// The command that would be run if permission is granted.
1655    pub command: String,
1656    /// The absolute working directory for the command.
1657    pub cwd: AbsolutePath,
1658    /// The associated tool call, when known. Omitted and `null` are equivalent.
1659    #[serde_as(deserialize_as = "DefaultOnError")]
1660    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1661    #[serde(default)]
1662    pub tool_call_id: Option<ToolCallId>,
1663    /// The associated terminal, when already known. Omitted and `null` are equivalent.
1664    #[serde_as(deserialize_as = "DefaultOnError")]
1665    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1666    #[serde(default)]
1667    pub terminal_id: Option<TerminalId>,
1668    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1669    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1670    /// these keys. Omitted and `null` are equivalent and mean no subject metadata was provided.
1671    ///
1672    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1673    #[serde_as(deserialize_as = "DefaultOnError")]
1674    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1675    #[serde(default)]
1676    #[serde(rename = "_meta")]
1677    pub meta: Option<Meta>,
1678}
1679
1680impl CommandPermissionSubject {
1681    /// Builds command permission details with the required command and working directory.
1682    #[must_use]
1683    pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
1684        Self {
1685            command: command.into(),
1686            cwd: cwd.into(),
1687            tool_call_id: None,
1688            terminal_id: None,
1689            meta: None,
1690        }
1691    }
1692
1693    /// Sets or clears the associated tool-call ID.
1694    #[must_use]
1695    pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
1696        self.tool_call_id = tool_call_id.into_option();
1697        self
1698    }
1699
1700    /// Sets or clears the associated terminal ID.
1701    #[must_use]
1702    pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
1703        self.terminal_id = terminal_id.into_option();
1704        self
1705    }
1706
1707    /// Sets or clears subject-scoped metadata.
1708    #[must_use]
1709    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1710        self.meta = meta.into_option();
1711        self
1712    }
1713}
1714
1715/// Custom or future permission subject payload.
1716#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1717#[derive(Debug, Clone, Serialize, PartialEq)]
1718#[cfg_attr(feature = "schemars", schemars(inline))]
1719#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
1720#[serde(rename_all = "camelCase")]
1721#[non_exhaustive]
1722pub struct OtherRequestPermissionSubject {
1723    /// Custom or future permission subject type.
1724    ///
1725    /// Values beginning with `_` are reserved for implementation-specific
1726    /// extensions. Unknown values that do not begin with `_` are reserved for
1727    /// future ACP variants.
1728    #[serde(rename = "type")]
1729    pub type_: String,
1730    /// Additional fields from the unknown permission subject payload.
1731    #[serde(flatten)]
1732    pub fields: BTreeMap<String, serde_json::Value>,
1733}
1734
1735impl OtherRequestPermissionSubject {
1736    /// Builds [`OtherRequestPermissionSubject`] from an unknown discriminator and preserves the remaining extension fields.
1737    #[must_use]
1738    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1739        fields.remove("type");
1740        Self {
1741            type_: type_.into(),
1742            fields,
1743        }
1744    }
1745}
1746
1747impl<'de> Deserialize<'de> for OtherRequestPermissionSubject {
1748    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1749    where
1750        D: serde::Deserializer<'de>,
1751    {
1752        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1753        let type_ = fields
1754            .remove("type")
1755            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
1756        let serde_json::Value::String(type_) = type_ else {
1757            return Err(serde::de::Error::custom("`type` must be a string"));
1758        };
1759
1760        if is_known_request_permission_subject_type(&type_) {
1761            return Err(serde::de::Error::custom(format!(
1762                "known request permission subject `{type_}` did not match its schema"
1763            )));
1764        }
1765
1766        Ok(Self { type_, fields })
1767    }
1768}
1769
1770fn is_known_request_permission_subject_type(type_: &str) -> bool {
1771    matches!(type_, "tool_call" | "command")
1772}
1773
1774#[cfg(feature = "schemars")]
1775fn other_request_permission_subject_schema(schema: &mut Schema) {
1776    super::schema_util::reject_known_string_discriminators(
1777        schema,
1778        "type",
1779        &["tool_call", "command"],
1780    );
1781}
1782
1783/// An option presented to the user when requesting permission.
1784#[serde_as]
1785#[skip_serializing_none]
1786#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1787#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1788#[serde(rename_all = "camelCase")]
1789#[non_exhaustive]
1790pub struct PermissionOption {
1791    /// Unique identifier for this permission option.
1792    pub option_id: PermissionOptionId,
1793    /// Human-readable label to display to the user.
1794    pub name: String,
1795    /// Hint about the nature of this permission option.
1796    pub kind: PermissionOptionKind,
1797    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1798    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1799    /// these keys.
1800    ///
1801    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1802    #[serde_as(deserialize_as = "DefaultOnError")]
1803    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1804    #[serde(default)]
1805    #[serde(rename = "_meta")]
1806    pub meta: Option<Meta>,
1807}
1808
1809impl PermissionOption {
1810    /// Builds [`PermissionOption`] with the required fields set; optional fields start unset or empty.
1811    #[must_use]
1812    pub fn new(
1813        option_id: impl Into<PermissionOptionId>,
1814        name: impl Into<String>,
1815        kind: PermissionOptionKind,
1816    ) -> Self {
1817        Self {
1818            option_id: option_id.into(),
1819            name: name.into(),
1820            kind,
1821            meta: None,
1822        }
1823    }
1824
1825    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1826    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1827    /// these keys.
1828    ///
1829    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1830    #[must_use]
1831    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1832        self.meta = meta.into_option();
1833        self
1834    }
1835}
1836
1837/// Unique identifier for a permission option.
1838#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1839#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
1840#[serde(transparent)]
1841#[from(forward)]
1842#[non_exhaustive]
1843pub struct PermissionOptionId(pub Arc<str>);
1844
1845impl PermissionOptionId {
1846    /// Wraps a protocol string as a typed [`PermissionOptionId`].
1847    #[must_use]
1848    pub fn new(id: impl Into<Self>) -> Self {
1849        id.into()
1850    }
1851}
1852
1853/// The type of permission option being presented to the user.
1854///
1855/// Helps clients choose appropriate icons and UI treatment.
1856#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1857#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1858#[serde(rename_all = "snake_case")]
1859#[non_exhaustive]
1860pub enum PermissionOptionKind {
1861    /// Allow this operation only this time.
1862    AllowOnce,
1863    /// Allow this operation and remember the choice.
1864    AllowAlways,
1865    /// Reject this operation only this time.
1866    RejectOnce,
1867    /// Reject this operation and remember the choice.
1868    RejectAlways,
1869    /// Custom or future permission option kind.
1870    ///
1871    /// Values beginning with `_` are reserved for implementation-specific
1872    /// extensions. Unknown values that do not begin with `_` are reserved for
1873    /// future ACP variants.
1874    #[serde(untagged)]
1875    Other(String),
1876}
1877
1878/// Response to a permission request.
1879#[serde_as]
1880#[skip_serializing_none]
1881#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1882#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1883#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
1884#[serde(rename_all = "camelCase")]
1885#[non_exhaustive]
1886pub struct RequestPermissionResponse {
1887    /// The user's decision on the permission request.
1888    pub outcome: RequestPermissionOutcome,
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.
1892    ///
1893    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1894    #[serde_as(deserialize_as = "DefaultOnError")]
1895    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1896    #[serde(default)]
1897    #[serde(rename = "_meta")]
1898    pub meta: Option<Meta>,
1899}
1900
1901impl RequestPermissionResponse {
1902    /// Builds [`RequestPermissionResponse`] with the required response fields set; optional fields start unset or empty.
1903    #[must_use]
1904    pub fn new(outcome: RequestPermissionOutcome) -> Self {
1905        Self {
1906            outcome,
1907            meta: None,
1908        }
1909    }
1910
1911    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1912    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1913    /// these keys.
1914    ///
1915    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1916    #[must_use]
1917    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1918        self.meta = meta.into_option();
1919        self
1920    }
1921}
1922
1923/// The outcome of a permission request.
1924#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1925#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1926#[serde(tag = "outcome", rename_all = "snake_case")]
1927#[non_exhaustive]
1928pub enum RequestPermissionOutcome {
1929    /// Active session work was cancelled before the user responded.
1930    ///
1931    /// When a client sends a `session/cancel` notification to cancel active
1932    /// session work, it MUST respond to all pending `session/request_permission`
1933    /// requests with this `Cancelled` outcome.
1934    ///
1935    /// See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-lifecycle#cancellation)
1936    Cancelled,
1937    /// The user selected one of the provided options.
1938    #[serde(rename_all = "camelCase")]
1939    Selected(SelectedPermissionOutcome),
1940    /// Custom or future permission outcome.
1941    ///
1942    /// Values beginning with `_` are reserved for implementation-specific
1943    /// extensions. Unknown values that do not begin with `_` are reserved for
1944    /// future ACP variants.
1945    ///
1946    /// Agents that do not understand this outcome MUST NOT treat it as approval.
1947    /// They should preserve the raw payload when storing, replaying, proxying, or
1948    /// forwarding permission responses, and otherwise fail or decline the
1949    /// permission request according to policy.
1950    #[serde(untagged)]
1951    Other(OtherRequestPermissionOutcome),
1952}
1953
1954/// Custom or future permission outcome payload.
1955///
1956/// This preserves the unknown `outcome` discriminator and the rest of the
1957/// outcome object for agents that store, replay, proxy, or forward permission
1958/// responses.
1959#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1960#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1961#[cfg_attr(feature = "schemars", schemars(inline))]
1962#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
1963#[serde(rename_all = "camelCase")]
1964#[non_exhaustive]
1965pub struct OtherRequestPermissionOutcome {
1966    /// Custom or future permission outcome.
1967    ///
1968    /// Values beginning with `_` are reserved for implementation-specific
1969    /// extensions. Unknown values that do not begin with `_` are reserved for
1970    /// future ACP variants.
1971    pub outcome: String,
1972    /// Additional fields from the unknown permission outcome payload.
1973    #[serde(flatten)]
1974    pub fields: BTreeMap<String, serde_json::Value>,
1975}
1976
1977impl OtherRequestPermissionOutcome {
1978    /// Builds [`OtherRequestPermissionOutcome`] from an unknown discriminator and preserves the remaining extension fields.
1979    #[must_use]
1980    pub fn new(
1981        outcome: impl Into<String>,
1982        mut fields: BTreeMap<String, serde_json::Value>,
1983    ) -> Self {
1984        fields.remove("outcome");
1985        Self {
1986            outcome: outcome.into(),
1987            fields,
1988        }
1989    }
1990}
1991
1992impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
1993    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1994    where
1995        D: serde::Deserializer<'de>,
1996    {
1997        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1998        let outcome = fields
1999            .remove("outcome")
2000            .ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
2001        let serde_json::Value::String(outcome) = outcome else {
2002            return Err(serde::de::Error::custom("`outcome` must be a string"));
2003        };
2004
2005        if is_known_request_permission_outcome(&outcome) {
2006            return Err(serde::de::Error::custom(format!(
2007                "known request permission outcome `{outcome}` did not match its schema"
2008            )));
2009        }
2010
2011        Ok(Self { outcome, fields })
2012    }
2013}
2014
2015fn is_known_request_permission_outcome(outcome: &str) -> bool {
2016    matches!(outcome, "cancelled" | "selected")
2017}
2018
2019#[cfg(feature = "schemars")]
2020fn other_request_permission_outcome_schema(schema: &mut Schema) {
2021    super::schema_util::reject_known_string_discriminators(
2022        schema,
2023        "outcome",
2024        &["cancelled", "selected"],
2025    );
2026}
2027
2028/// The user selected one of the provided options.
2029#[serde_as]
2030#[skip_serializing_none]
2031#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2032#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2033#[serde(rename_all = "camelCase")]
2034#[non_exhaustive]
2035pub struct SelectedPermissionOutcome {
2036    /// The ID of the option the user selected.
2037    pub option_id: PermissionOptionId,
2038    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2039    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2040    /// these keys.
2041    ///
2042    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2043    #[serde_as(deserialize_as = "DefaultOnError")]
2044    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2045    #[serde(default)]
2046    #[serde(rename = "_meta")]
2047    pub meta: Option<Meta>,
2048}
2049
2050impl SelectedPermissionOutcome {
2051    /// Builds [`SelectedPermissionOutcome`] with the required fields set; optional fields start unset or empty.
2052    #[must_use]
2053    pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
2054        Self {
2055            option_id: option_id.into(),
2056            meta: None,
2057        }
2058    }
2059
2060    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2061    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2062    /// these keys.
2063    ///
2064    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2065    #[must_use]
2066    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2067        self.meta = meta.into_option();
2068        self
2069    }
2070}
2071
2072// Capabilities
2073
2074/// Capabilities supported by the client.
2075///
2076/// Advertised during initialization to inform the agent about
2077/// available features and methods.
2078///
2079/// See protocol docs: [Client Capabilities](https://agentclientprotocol.com/protocol/initialization#client-capabilities)
2080#[serde_as]
2081#[skip_serializing_none]
2082#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2083#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2084#[serde(rename_all = "camelCase")]
2085#[non_exhaustive]
2086pub struct ClientCapabilities {
2087    /// Authentication capabilities supported by the client.
2088    /// Determines which authentication method types the agent may include
2089    /// in its `InitializeResponse`.
2090    ///
2091    /// Optional. Omitted or `null` both mean the client does not advertise any
2092    /// authentication-method extensions.
2093    #[serde_as(deserialize_as = "DefaultOnError")]
2094    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2095    #[serde(default)]
2096    pub auth: Option<AuthCapabilities>,
2097    /// Elicitation capabilities supported by the client.
2098    /// Determines which elicitation modes the agent may use.
2099    ///
2100    /// Optional. Omitted or `null` both mean the client does not advertise
2101    /// elicitation support.
2102    #[serde_as(deserialize_as = "DefaultOnError")]
2103    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2104    #[serde(default)]
2105    pub elicitation: Option<ElicitationCapabilities>,
2106    /// **UNSTABLE**
2107    ///
2108    /// This capability is not part of the spec yet, and may be removed or changed at any point.
2109    ///
2110    /// NES (Next Edit Suggestions) capabilities supported by the client.
2111    ///
2112    /// Optional. Omitted or `null` both mean the client does not advertise any
2113    /// NES suggestion-kind extensions.
2114    #[cfg(feature = "unstable_nes")]
2115    #[serde_as(deserialize_as = "DefaultOnError")]
2116    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2117    #[serde(default)]
2118    pub nes: Option<ClientNesCapabilities>,
2119    /// **UNSTABLE**
2120    ///
2121    /// This capability is not part of the spec yet, and may be removed or changed at any point.
2122    ///
2123    /// The position encodings supported by the client, in order of preference.
2124    #[cfg(feature = "unstable_nes")]
2125    #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
2126    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2127    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2128    pub position_encodings: Vec<PositionEncodingKind>,
2129
2130    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2131    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2132    /// these keys.
2133    ///
2134    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2135    #[serde_as(deserialize_as = "DefaultOnError")]
2136    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2137    #[serde(default)]
2138    #[serde(rename = "_meta")]
2139    pub meta: Option<Meta>,
2140}
2141
2142impl ClientCapabilities {
2143    /// Builds an empty [`ClientCapabilities`]; use builder methods to advertise supported sub-capabilities.
2144    #[must_use]
2145    pub fn new() -> Self {
2146        Self::default()
2147    }
2148
2149    /// Authentication capabilities supported by the client.
2150    /// Determines which authentication method types the agent may include
2151    /// in its `InitializeResponse`.
2152    #[must_use]
2153    pub fn auth(mut self, auth: impl IntoOption<AuthCapabilities>) -> Self {
2154        self.auth = auth.into_option();
2155        self
2156    }
2157
2158    /// Elicitation capabilities supported by the client.
2159    /// Determines which elicitation modes the agent may use.
2160    #[must_use]
2161    pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
2162        self.elicitation = elicitation.into_option();
2163        self
2164    }
2165
2166    /// **UNSTABLE**
2167    ///
2168    /// NES (Next Edit Suggestions) capabilities supported by the client.
2169    #[cfg(feature = "unstable_nes")]
2170    #[must_use]
2171    pub fn nes(mut self, nes: impl IntoOption<ClientNesCapabilities>) -> Self {
2172        self.nes = nes.into_option();
2173        self
2174    }
2175
2176    /// **UNSTABLE**
2177    ///
2178    /// The position encodings supported by the client, in order of preference.
2179    #[cfg(feature = "unstable_nes")]
2180    #[must_use]
2181    pub fn position_encodings(mut self, position_encodings: Vec<PositionEncodingKind>) -> Self {
2182        self.position_encodings = position_encodings;
2183        self
2184    }
2185
2186    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2187    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2188    /// these keys.
2189    ///
2190    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2191    #[must_use]
2192    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2193        self.meta = meta.into_option();
2194        self
2195    }
2196}
2197
2198/// Authentication capabilities supported by the client.
2199///
2200/// Advertised during initialization to inform the agent which authentication
2201/// method types the client can handle. This governs opt-in types that require
2202/// additional client-side support.
2203#[serde_as]
2204#[skip_serializing_none]
2205#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2206#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2207#[serde(rename_all = "camelCase")]
2208#[non_exhaustive]
2209pub struct AuthCapabilities {
2210    /// Whether the client supports `terminal` authentication methods.
2211    ///
2212    /// Optional. Omitted or `null` both mean the client does not advertise support.
2213    /// The client should supply `{}` only when it can reproduce the configured
2214    /// agent invocation in an interactive terminal. Supplying `{}` means the
2215    /// agent may include `terminal` entries in its authentication methods.
2216    #[serde_as(deserialize_as = "DefaultOnError")]
2217    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2218    #[serde(default)]
2219    pub terminal: Option<TerminalAuthCapabilities>,
2220    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2221    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2222    /// these keys.
2223    ///
2224    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2225    #[serde_as(deserialize_as = "DefaultOnError")]
2226    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2227    #[serde(default)]
2228    #[serde(rename = "_meta")]
2229    pub meta: Option<Meta>,
2230}
2231
2232impl AuthCapabilities {
2233    /// Builds an empty [`AuthCapabilities`]; use builder methods to advertise supported sub-capabilities.
2234    #[must_use]
2235    pub fn new() -> Self {
2236        Self::default()
2237    }
2238
2239    /// Whether the client supports `terminal` authentication methods.
2240    ///
2241    /// Omitted or `null` both mean the client does not advertise support.
2242    /// The client should supply `{}` only when it can reproduce the configured
2243    /// agent invocation in an interactive terminal. Supplying `{}` means the
2244    /// agent may include `AuthMethod::Terminal` entries in its authentication
2245    /// methods.
2246    #[must_use]
2247    pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
2248        self.terminal = terminal.into_option();
2249        self
2250    }
2251
2252    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2253    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2254    /// these keys.
2255    ///
2256    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2257    #[must_use]
2258    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2259        self.meta = meta.into_option();
2260        self
2261    }
2262}
2263
2264/// Capabilities for terminal authentication methods.
2265///
2266/// Supplying `{}` means the client can reproduce the configured agent
2267/// invocation in an interactive terminal and supports terminal authentication
2268/// methods.
2269#[serde_as]
2270#[skip_serializing_none]
2271#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2272#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2273#[non_exhaustive]
2274pub struct TerminalAuthCapabilities {
2275    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2276    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2277    /// these keys.
2278    ///
2279    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2280    #[serde_as(deserialize_as = "DefaultOnError")]
2281    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2282    #[serde(default)]
2283    #[serde(rename = "_meta")]
2284    pub meta: Option<Meta>,
2285}
2286
2287impl TerminalAuthCapabilities {
2288    /// Builds an empty [`TerminalAuthCapabilities`]; use builder methods to advertise supported sub-capabilities.
2289    #[must_use]
2290    pub fn new() -> Self {
2291        Self::default()
2292    }
2293
2294    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
2295    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
2296    /// these keys.
2297    ///
2298    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2299    #[must_use]
2300    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2301        self.meta = meta.into_option();
2302        self
2303    }
2304}
2305
2306// Method schema
2307
2308/// Names of all methods that clients handle.
2309///
2310/// Provides a centralized definition of method names used in the protocol.
2311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2312#[non_exhaustive]
2313pub struct ClientMethodNames {
2314    /// Method for requesting permission from the user.
2315    pub session_request_permission: &'static str,
2316    /// Notification for session updates.
2317    pub session_update: &'static str,
2318    /// Method for opening an MCP-over-ACP connection.
2319    #[cfg(feature = "unstable_mcp_over_acp")]
2320    pub mcp_connect: &'static str,
2321    /// Method for exchanging MCP-over-ACP messages.
2322    #[cfg(feature = "unstable_mcp_over_acp")]
2323    pub mcp_message: &'static str,
2324    /// Method for closing an MCP-over-ACP connection.
2325    #[cfg(feature = "unstable_mcp_over_acp")]
2326    pub mcp_disconnect: &'static str,
2327    /// Method for elicitation.
2328    pub elicitation_create: &'static str,
2329    /// Notification for elicitation completion.
2330    pub elicitation_complete: &'static str,
2331}
2332
2333/// Constant containing all client method names.
2334pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
2335    session_update: SESSION_UPDATE_NOTIFICATION,
2336    session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
2337    #[cfg(feature = "unstable_mcp_over_acp")]
2338    mcp_connect: MCP_CONNECT_METHOD_NAME,
2339    #[cfg(feature = "unstable_mcp_over_acp")]
2340    mcp_message: MCP_MESSAGE_METHOD_NAME,
2341    #[cfg(feature = "unstable_mcp_over_acp")]
2342    mcp_disconnect: MCP_DISCONNECT_METHOD_NAME,
2343    elicitation_create: ELICITATION_CREATE_METHOD_NAME,
2344    elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
2345};
2346
2347/// Notification name for session updates.
2348pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
2349/// Method name for requesting user permission.
2350pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
2351/// Method name for elicitation.
2352pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
2353/// Notification name for elicitation completion.
2354pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
2355
2356/// All possible requests that an agent can send to a client.
2357///
2358/// This enum is used internally for routing RPC requests. You typically won't need
2359/// to use this directly.
2360///
2361/// This enum encompasses all method calls from agent to client.
2362#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2363#[derive(Clone, Debug, Serialize, Deserialize)]
2364#[serde(untagged)]
2365#[cfg_attr(feature = "schemars", schemars(inline))]
2366#[non_exhaustive]
2367pub enum AgentRequest {
2368    /// Requests permission from the user for an operation.
2369    ///
2370    /// Called by the agent when it needs user authorization before executing
2371    /// a potentially sensitive operation. The client should present the options
2372    /// to the user and return their decision.
2373    ///
2374    /// If the client cancels active session work via `session/cancel`, it MUST
2375    /// respond to this request with `RequestPermissionOutcome::Cancelled`.
2376    ///
2377    /// See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission)
2378    RequestPermissionRequest(Box<RequestPermissionRequest>),
2379    /// Requests structured user input via a form or URL.
2380    ///
2381    /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/elicitation)
2382    CreateElicitationRequest(Box<CreateElicitationRequest>),
2383    /// **UNSTABLE**
2384    ///
2385    /// This capability is not part of the spec yet, and may be removed or changed at any point.
2386    ///
2387    /// Opens an MCP-over-ACP connection.
2388    #[cfg(feature = "unstable_mcp_over_acp")]
2389    ConnectMcpRequest(Box<ConnectMcpRequest>),
2390    /// **UNSTABLE**
2391    ///
2392    /// This capability is not part of the spec yet, and may be removed or changed at any point.
2393    ///
2394    /// Exchanges an MCP-over-ACP message.
2395    #[cfg(feature = "unstable_mcp_over_acp")]
2396    MessageMcpRequest(Box<MessageMcpRequest>),
2397    /// **UNSTABLE**
2398    ///
2399    /// This capability is not part of the spec yet, and may be removed or changed at any point.
2400    ///
2401    /// Closes an MCP-over-ACP connection.
2402    #[cfg(feature = "unstable_mcp_over_acp")]
2403    DisconnectMcpRequest(Box<DisconnectMcpRequest>),
2404    /// Handles extension method requests from the agent.
2405    ///
2406    /// Allows the Agent to send an arbitrary request that is not part of the ACP spec.
2407    /// Extension methods provide a way to add custom functionality while maintaining
2408    /// protocol compatibility.
2409    ///
2410    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2411    ExtMethodRequest(Box<ExtRequest>),
2412}
2413
2414impl AgentRequest {
2415    /// Returns the corresponding method name of the request.
2416    #[must_use]
2417    pub fn method(&self) -> &str {
2418        match self {
2419            Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
2420            Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
2421            #[cfg(feature = "unstable_mcp_over_acp")]
2422            Self::ConnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_connect,
2423            #[cfg(feature = "unstable_mcp_over_acp")]
2424            Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
2425            #[cfg(feature = "unstable_mcp_over_acp")]
2426            Self::DisconnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_disconnect,
2427            Self::ExtMethodRequest(ext_request) => &ext_request.method,
2428        }
2429    }
2430}
2431
2432/// All possible responses that a client can send to an agent.
2433///
2434/// This enum is used internally for routing RPC responses. You typically won't need
2435/// to use this directly - the responses are handled automatically by the connection.
2436///
2437/// These are responses to the corresponding `AgentRequest` variants.
2438#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2439#[derive(Clone, Debug, Serialize, Deserialize)]
2440#[serde(untagged)]
2441#[cfg_attr(feature = "schemars", schemars(inline))]
2442#[non_exhaustive]
2443pub enum ClientResponse {
2444    /// Successful result returned for a `session/request_permission` request.
2445    RequestPermissionResponse(Box<RequestPermissionResponse>),
2446    /// Successful result returned for a `elicitation/create` request.
2447    CreateElicitationResponse(Box<CreateElicitationResponse>),
2448    /// Successful result returned for a `mcp/connect` request.
2449    #[cfg(feature = "unstable_mcp_over_acp")]
2450    ConnectMcpResponse(Box<ConnectMcpResponse>),
2451    /// Successful result returned for a `mcp/disconnect` request.
2452    #[cfg(feature = "unstable_mcp_over_acp")]
2453    DisconnectMcpResponse(#[serde(default)] Box<DisconnectMcpResponse>),
2454    /// Successful result returned by an MCP-over-ACP `mcp/message` request.
2455    #[cfg(feature = "unstable_mcp_over_acp")]
2456    MessageMcpResponse(Box<MessageMcpResponse>),
2457    /// Successful result returned by an extension method outside the core ACP method set.
2458    ExtMethodResponse(Box<ExtResponse>),
2459}
2460
2461/// All possible notifications that an agent can send to a client.
2462///
2463/// This enum is used internally for routing RPC notifications. You typically won't need
2464/// to use this directly.
2465///
2466/// Notifications do not expect a response.
2467#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2468#[derive(Clone, Debug, Serialize, Deserialize)]
2469#[serde(untagged)]
2470#[cfg_attr(feature = "schemars", schemars(inline))]
2471#[non_exhaustive]
2472pub enum AgentNotification {
2473    /// Handles session update notifications from the agent.
2474    ///
2475    /// This is a notification endpoint (no response expected) that receives
2476    /// updates about session activity, including message updates, message chunks,
2477    /// tool calls, and execution plans.
2478    ///
2479    /// Note: Clients SHOULD continue accepting tool call updates even after
2480    /// sending a `session/cancel` notification, as the agent may send final
2481    /// updates before reporting an idle `state_update` with the cancelled
2482    /// stop reason.
2483    ///
2484    /// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-lifecycle#3-agent-reports-output)
2485    UpdateSessionNotification(Box<UpdateSessionNotification>),
2486    /// Notification that a URL-based elicitation has completed.
2487    ///
2488    /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/elicitation#url-completion)
2489    CompleteElicitationNotification(Box<CompleteElicitationNotification>),
2490    /// **UNSTABLE**
2491    ///
2492    /// This capability is not part of the spec yet, and may be removed or changed at any point.
2493    ///
2494    /// Receives an MCP-over-ACP notification.
2495    #[cfg(feature = "unstable_mcp_over_acp")]
2496    MessageMcpNotification(Box<MessageMcpNotification>),
2497    /// Handles extension notifications from the agent.
2498    ///
2499    /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec.
2500    /// Extension notifications provide a way to send one-way messages for custom functionality
2501    /// while maintaining protocol compatibility.
2502    ///
2503    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
2504    ExtNotification(Box<ExtNotification>),
2505}
2506
2507impl AgentNotification {
2508    /// Returns the corresponding method name of the notification.
2509    #[must_use]
2510    pub fn method(&self) -> &str {
2511        match self {
2512            Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
2513            Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
2514            #[cfg(feature = "unstable_mcp_over_acp")]
2515            Self::MessageMcpNotification(_) => CLIENT_METHOD_NAMES.mcp_message,
2516            Self::ExtNotification(ext_notification) => &ext_notification.method,
2517        }
2518    }
2519}
2520
2521#[cfg(test)]
2522mod tests {
2523    use super::*;
2524
2525    #[cfg(feature = "unstable_session_compaction")]
2526    #[test]
2527    fn compaction_updates_preserve_patch_and_open_status_semantics() {
2528        use serde_json::json;
2529
2530        assert_eq!(
2531            serde_json::to_value(SessionUpdate::CompactionUpdate(
2532                CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
2533                    ContentBlock::Text(crate::v2::TextContent::new("retained")),
2534                ]),
2535            ))
2536            .unwrap(),
2537            json!({
2538                "sessionUpdate": "compaction_update",
2539                "compactionId": "cmp_001",
2540                "status": "completed",
2541                "summary": [{ "type": "text", "text": "retained" }]
2542            })
2543        );
2544
2545        let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
2546            "sessionUpdate": "compaction_update",
2547            "compactionId": "cmp_001",
2548            "status": "paused",
2549            "summary": null
2550        }))
2551        .unwrap() else {
2552            panic!("expected compaction update");
2553        };
2554        assert_eq!(update.status, CompactionStatus::Other("paused".into()));
2555        assert!(update.summary.is_null());
2556        assert!(update.error.is_undefined());
2557    }
2558
2559    #[cfg(feature = "unstable_session_compaction")]
2560    #[test]
2561    fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
2562        use serde_json::json;
2563
2564        assert!(
2565            serde_json::from_value::<SessionUpdate>(json!({
2566                "sessionUpdate": "compaction_update",
2567                "status": "completed"
2568            }))
2569            .is_err()
2570        );
2571        assert!(
2572            serde_json::from_value::<SessionUpdate>(json!({
2573                "sessionUpdate": "compaction_summary_chunk",
2574                "compactionId": "cmp_001"
2575            }))
2576            .is_err()
2577        );
2578    }
2579
2580    #[test]
2581    fn test_elicitation_capability_semantics() {
2582        use serde_json::json;
2583
2584        let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
2585        assert!(unsupported.elicitation.is_none());
2586
2587        let null: ClientCapabilities =
2588            serde_json::from_value(json!({ "elicitation": null })).unwrap();
2589        assert!(null.elicitation.is_none());
2590
2591        let malformed: ClientCapabilities =
2592            serde_json::from_value(json!({ "elicitation": false })).unwrap();
2593        assert!(malformed.elicitation.is_none());
2594
2595        let empty: ClientCapabilities =
2596            serde_json::from_value(json!({ "elicitation": {} })).unwrap();
2597        let empty = empty.elicitation.expect("present capability");
2598        assert!(!empty.supports_form());
2599        assert!(!empty.supports_url());
2600
2601        let form_only: ClientCapabilities = serde_json::from_value(json!({
2602            "elicitation": { "form": {} }
2603        }))
2604        .unwrap();
2605        let form_only = form_only.elicitation.expect("advertised capability");
2606        assert!(form_only.supports_form());
2607        assert!(!form_only.supports_url());
2608
2609        let url_only: ClientCapabilities = serde_json::from_value(json!({
2610            "elicitation": { "url": {} }
2611        }))
2612        .unwrap();
2613        let url_only = url_only.elicitation.expect("advertised capability");
2614        assert!(!url_only.supports_form());
2615        assert!(url_only.supports_url());
2616
2617        let both: ClientCapabilities = serde_json::from_value(json!({
2618            "elicitation": { "form": {}, "url": {} }
2619        }))
2620        .unwrap();
2621        let both = both.elicitation.expect("advertised capability");
2622        assert!(both.supports_form());
2623        assert!(both.supports_url());
2624    }
2625
2626    #[test]
2627    fn test_elicitation_method_routing_and_envelopes() {
2628        use serde_json::json;
2629
2630        assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
2631        assert_eq!(
2632            CLIENT_METHOD_NAMES.elicitation_complete,
2633            "elicitation/complete"
2634        );
2635
2636        let request =
2637            AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
2638                crate::v2::ElicitationFormMode::new(
2639                    crate::v2::ElicitationSessionScope::new("sess_1"),
2640                    crate::v2::ElicitationSchema::new(),
2641                ),
2642                "Choose a value",
2643            )));
2644        assert_eq!(request.method(), "elicitation/create");
2645        let method = Arc::from(request.method());
2646        let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
2647            id: crate::v2::RequestId::Number(7),
2648            method,
2649            params: Some(request),
2650        });
2651        assert_eq!(
2652            serde_json::to_value(request).unwrap(),
2653            json!({
2654                "jsonrpc": "2.0",
2655                "id": 7,
2656                "method": "elicitation/create",
2657                "params": {
2658                    "mode": "form",
2659                    "sessionId": "sess_1",
2660                    "message": "Choose a value",
2661                    "requestedSchema": { "type": "object", "properties": {} }
2662                }
2663            })
2664        );
2665
2666        let notification = AgentNotification::CompleteElicitationNotification(Box::new(
2667            CompleteElicitationNotification::new("elic_1"),
2668        ));
2669        assert_eq!(notification.method(), "elicitation/complete");
2670        let method = Arc::from(notification.method());
2671        let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
2672            method,
2673            params: Some(notification),
2674        });
2675        assert_eq!(
2676            serde_json::to_value(notification).unwrap(),
2677            json!({
2678                "jsonrpc": "2.0",
2679                "method": "elicitation/complete",
2680                "params": { "elicitationId": "elic_1" }
2681            })
2682        );
2683    }
2684
2685    #[test]
2686    fn test_client_capabilities_auth_defaults_on_malformed_value() {
2687        use serde_json::json;
2688
2689        let capabilities: ClientCapabilities = serde_json::from_value(json!({
2690            "auth": false
2691        }))
2692        .unwrap();
2693
2694        assert_eq!(capabilities.auth, None);
2695    }
2696
2697    #[test]
2698    fn test_serialization_behavior() {
2699        use serde_json::json;
2700
2701        assert_eq!(
2702            serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
2703            SessionInfoUpdate {
2704                title: MaybeUndefined::Undefined,
2705                updated_at: MaybeUndefined::Undefined,
2706                meta: MaybeUndefined::Undefined
2707            }
2708        );
2709        assert_eq!(
2710            serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
2711                .unwrap(),
2712            SessionInfoUpdate {
2713                title: MaybeUndefined::Null,
2714                updated_at: MaybeUndefined::Null,
2715                meta: MaybeUndefined::Undefined
2716            }
2717        );
2718        assert_eq!(
2719            serde_json::from_value::<SessionInfoUpdate>(
2720                json!({"title": "title", "updatedAt": "timestamp"})
2721            )
2722            .unwrap(),
2723            SessionInfoUpdate {
2724                title: MaybeUndefined::Value("title".to_string()),
2725                updated_at: MaybeUndefined::Value("timestamp".to_string()),
2726                meta: MaybeUndefined::Undefined
2727            }
2728        );
2729
2730        let clear_meta =
2731            serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
2732        assert_eq!(clear_meta.meta, MaybeUndefined::Null);
2733
2734        let mut meta = Meta::new();
2735        meta.insert("source".to_string(), json!("session-info"));
2736
2737        assert_eq!(
2738            serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
2739                "source": "session-info"
2740            }}))
2741            .unwrap()
2742            .meta,
2743            MaybeUndefined::Value(meta.clone())
2744        );
2745
2746        assert_eq!(
2747            serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
2748            json!({})
2749        );
2750
2751        assert_eq!(
2752            serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
2753            json!({"_meta": null})
2754        );
2755
2756        assert_eq!(
2757            serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
2758            json!({"_meta": {
2759                "source": "session-info"
2760            }})
2761        );
2762        assert_eq!(
2763            serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
2764            json!({"title": "title"})
2765        );
2766        assert_eq!(
2767            serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
2768            json!({"title": null})
2769        );
2770        assert_eq!(
2771            serde_json::to_value(
2772                SessionInfoUpdate::new()
2773                    .title("title")
2774                    .title(MaybeUndefined::Undefined)
2775            )
2776            .unwrap(),
2777            json!({})
2778        );
2779    }
2780
2781    #[test]
2782    fn test_content_chunk_message_id_serialization() {
2783        use serde_json::json;
2784
2785        assert_eq!(
2786            serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
2787                ContentBlock::Text(crate::v2::TextContent::new("Hello")),
2788                "msg_agent_c42b9",
2789            )))
2790            .unwrap(),
2791            json!({
2792                "sessionUpdate": "agent_message_chunk",
2793                "messageId": "msg_agent_c42b9",
2794                "content": {
2795                    "type": "text",
2796                    "text": "Hello"
2797                }
2798            })
2799        );
2800
2801        let err = serde_json::from_value::<ContentChunk>(json!({
2802            "content": {
2803                "type": "text",
2804                "text": "Hello"
2805            }
2806        }))
2807        .unwrap_err();
2808
2809        assert!(err.to_string().contains("messageId"), "{err}");
2810    }
2811
2812    #[test]
2813    fn test_tool_call_content_chunk_serialization() {
2814        use serde_json::json;
2815
2816        assert_eq!(
2817            serde_json::to_value(SessionUpdate::ToolCallContentChunk(
2818                ToolCallContentChunk::new(
2819                    "call_001",
2820                    crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
2821                )
2822            ))
2823            .unwrap(),
2824            json!({
2825                "sessionUpdate": "tool_call_content_chunk",
2826                "toolCallId": "call_001",
2827                "content": {
2828                    "type": "content",
2829                    "content": {
2830                        "type": "text",
2831                        "text": "partial output"
2832                    }
2833                }
2834            })
2835        );
2836
2837        let err = serde_json::from_value::<ToolCallContentChunk>(json!({
2838            "content": {
2839                "type": "content",
2840                "content": {
2841                    "type": "text",
2842                    "text": "partial output"
2843                }
2844            }
2845        }))
2846        .unwrap_err();
2847
2848        assert!(err.to_string().contains("toolCallId"), "{err}");
2849    }
2850
2851    #[test]
2852    fn test_full_message_serialization() {
2853        use serde_json::json;
2854
2855        assert_eq!(
2856            serde_json::to_value(SessionUpdate::UserMessage(
2857                UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
2858                    crate::v2::TextContent::new("Hello")
2859                )])
2860            ))
2861            .unwrap(),
2862            json!({
2863                "sessionUpdate": "user_message",
2864                "messageId": "msg_user_8f7a1",
2865                "content": [
2866                    {
2867                        "type": "text",
2868                        "text": "Hello"
2869                    }
2870                ]
2871            })
2872        );
2873
2874        assert_eq!(
2875            serde_json::to_value(SessionUpdate::AgentMessage(
2876                AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
2877                    crate::v2::TextContent::new("Hello")
2878                )])
2879            ))
2880            .unwrap(),
2881            json!({
2882                "sessionUpdate": "agent_message",
2883                "messageId": "msg_agent_c42b9",
2884                "content": [
2885                    {
2886                        "type": "text",
2887                        "text": "Hello"
2888                    }
2889                ]
2890            })
2891        );
2892
2893        assert_eq!(
2894            serde_json::to_value(SessionUpdate::AgentThought(
2895                AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
2896                    crate::v2::TextContent::new("Need to inspect the call sites first.")
2897                )])
2898            ))
2899            .unwrap(),
2900            json!({
2901                "sessionUpdate": "agent_thought",
2902                "messageId": "msg_thought_a12",
2903                "content": [
2904                    {
2905                        "type": "text",
2906                        "text": "Need to inspect the call sites first."
2907                    }
2908                ]
2909            })
2910        );
2911    }
2912
2913    #[test]
2914    fn test_message_upsert_serialization() {
2915        use serde_json::json;
2916
2917        assert_eq!(
2918            serde_json::to_value(SessionUpdate::UserMessage(
2919                UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
2920            ))
2921            .unwrap(),
2922            json!({
2923                "sessionUpdate": "user_message",
2924                "messageId": "msg_empty",
2925                "content": []
2926            })
2927        );
2928
2929        let empty = serde_json::from_value::<UserMessage>(json!({
2930            "messageId": "msg_empty",
2931            "content": []
2932        }))
2933        .unwrap();
2934        assert!(matches!(
2935            empty.content,
2936            MaybeUndefined::Value(ref content) if content.is_empty()
2937        ));
2938
2939        let patch = serde_json::from_value::<AgentMessage>(json!({
2940            "messageId": "msg_agent_c42b9"
2941        }))
2942        .unwrap();
2943        assert_eq!(patch.content, MaybeUndefined::Undefined);
2944        assert_eq!(patch.meta, MaybeUndefined::Undefined);
2945
2946        let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
2947            "messageId": "msg_agent_c42b9",
2948            "_meta": false
2949        }))
2950        .unwrap();
2951        assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
2952
2953        let patch = serde_json::from_value::<AgentThought>(json!({
2954            "messageId": "msg_thought_a12"
2955        }))
2956        .unwrap();
2957        assert_eq!(patch.content, MaybeUndefined::Undefined);
2958
2959        let clear = serde_json::from_value::<UserMessage>(json!({
2960            "messageId": "msg_user_8f7a1",
2961            "content": null
2962        }))
2963        .unwrap();
2964        assert_eq!(clear.content, MaybeUndefined::Null);
2965
2966        let clear_meta = serde_json::from_value::<UserMessage>(json!({
2967            "messageId": "msg_user_8f7a1",
2968            "_meta": null
2969        }))
2970        .unwrap();
2971        assert_eq!(clear_meta.meta, MaybeUndefined::Null);
2972
2973        let mut meta = Meta::new();
2974        meta.insert("source".to_string(), json!("replay"));
2975
2976        assert_eq!(
2977            serde_json::to_value(SessionUpdate::UserMessage(
2978                UserMessage::new("msg_user_8f7a1").meta(meta)
2979            ))
2980            .unwrap(),
2981            json!({
2982                "sessionUpdate": "user_message",
2983                "messageId": "msg_user_8f7a1",
2984                "_meta": {
2985                    "source": "replay"
2986                }
2987            })
2988        );
2989
2990        assert_eq!(
2991            serde_json::to_value(SessionUpdate::UserMessage(
2992                UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
2993            ))
2994            .unwrap(),
2995            json!({
2996                "sessionUpdate": "user_message",
2997                "messageId": "msg_user_8f7a1",
2998                "_meta": null
2999            })
3000        );
3001    }
3002
3003    #[test]
3004    fn test_usage_update_serialization() {
3005        use serde_json::json;
3006
3007        assert_eq!(
3008            serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
3009                53_000, 200_000
3010            )))
3011            .unwrap(),
3012            json!({
3013                "sessionUpdate": "usage_update",
3014                "used": 53000,
3015                "size": 200_000
3016            })
3017        );
3018
3019        assert_eq!(
3020            serde_json::to_value(SessionUpdate::UsageUpdate(
3021                UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
3022            ))
3023            .unwrap(),
3024            json!({
3025                "sessionUpdate": "usage_update",
3026                "used": 53000,
3027                "size": 200_000,
3028                "cost": {
3029                    "amount": 0.045,
3030                    "currency": "USD"
3031                }
3032            })
3033        );
3034
3035        let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
3036            "sessionUpdate": "usage_update",
3037            "used": 53000,
3038            "size": 200_000,
3039            "cost": null
3040        }))
3041        .unwrap() else {
3042            panic!("expected usage update");
3043        };
3044
3045        assert_eq!(update.cost, None);
3046    }
3047
3048    #[test]
3049    fn test_state_update_serialization() {
3050        use serde_json::json;
3051
3052        assert_eq!(
3053            serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
3054                RunningStateUpdate::new()
3055            )))
3056            .unwrap(),
3057            json!({
3058                "sessionUpdate": "state_update",
3059                "state": "running"
3060            })
3061        );
3062
3063        assert_eq!(
3064            serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
3065                IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
3066            )))
3067            .unwrap(),
3068            json!({
3069                "sessionUpdate": "state_update",
3070                "state": "idle",
3071                "stopReason": "end_turn"
3072            })
3073        );
3074
3075        let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
3076            "sessionUpdate": "state_update",
3077            "state": "requires_action"
3078        }))
3079        .unwrap() else {
3080            panic!("expected state update");
3081        };
3082
3083        assert!(matches!(update, StateUpdate::RequiresAction(_)));
3084
3085        let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
3086            "sessionUpdate": "state_update",
3087            "state": "idle",
3088            "stopReason": null
3089        }))
3090        .unwrap() else {
3091            panic!("expected idle state update");
3092        };
3093
3094        assert_eq!(update.stop_reason, None);
3095
3096        let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
3097            serde_json::from_value(json!({
3098                "sessionUpdate": "state_update",
3099                "state": "_paused",
3100                "label": "Paused"
3101            }))
3102            .unwrap()
3103        else {
3104            panic!("expected unknown state update");
3105        };
3106
3107        assert_eq!(update.state, "_paused");
3108        assert_eq!(update.fields["label"], json!("Paused"));
3109    }
3110
3111    #[test]
3112    fn session_update_preserves_unknown_variant() {
3113        use serde_json::json;
3114
3115        let update: SessionUpdate = serde_json::from_value(json!({
3116            "sessionUpdate": "_status_badge",
3117            "label": "Indexing",
3118            "progress": 0.5
3119        }))
3120        .unwrap();
3121
3122        let SessionUpdate::Other(unknown) = update else {
3123            panic!("expected unknown session update");
3124        };
3125
3126        assert_eq!(unknown.session_update, "_status_badge");
3127        assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
3128        assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
3129
3130        assert_eq!(
3131            serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
3132            json!({
3133                "sessionUpdate": "_status_badge",
3134                "label": "Indexing",
3135                "progress": 0.5
3136            })
3137        );
3138    }
3139
3140    #[test]
3141    fn terminal_session_updates_use_known_discriminators() {
3142        use serde_json::json;
3143
3144        assert_eq!(
3145            serde_json::to_value(SessionUpdate::TerminalUpdate(
3146                TerminalUpdate::new("term_1").command("cargo test")
3147            ))
3148            .unwrap(),
3149            json!({
3150                "sessionUpdate": "terminal_update",
3151                "terminalId": "term_1",
3152                "command": "cargo test"
3153            })
3154        );
3155        assert_eq!(
3156            serde_json::to_value(SessionUpdate::TerminalOutputChunk(
3157                TerminalOutputChunk::new("term_1", "dGVzdAo=")
3158            ))
3159            .unwrap(),
3160            json!({
3161                "sessionUpdate": "terminal_output_chunk",
3162                "terminalId": "term_1",
3163                "data": "dGVzdAo="
3164            })
3165        );
3166    }
3167
3168    #[test]
3169    fn session_update_does_not_hide_malformed_known_terminal_variants() {
3170        use serde_json::json;
3171
3172        assert!(
3173            serde_json::from_value::<SessionUpdate>(json!({
3174                "sessionUpdate": "terminal_update"
3175            }))
3176            .is_err()
3177        );
3178        assert!(
3179            serde_json::from_value::<SessionUpdate>(json!({
3180                "sessionUpdate": "terminal_output_chunk",
3181                "terminalId": "term_1"
3182            }))
3183            .is_err()
3184        );
3185    }
3186
3187    #[test]
3188    fn test_plan_update_serialization() {
3189        use serde_json::json;
3190
3191        let plan_update =
3192            SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
3193                "plan-1",
3194                vec![crate::v2::PlanEntry::new(
3195                    "Step 1",
3196                    crate::v2::PlanEntryPriority::High,
3197                    crate::v2::PlanEntryStatus::Pending,
3198                )],
3199            )));
3200
3201        assert_eq!(
3202            serde_json::to_value(plan_update).unwrap(),
3203            json!({
3204                "sessionUpdate": "plan_update",
3205                "plan": {
3206                    "type": "items",
3207                    "planId": "plan-1",
3208                    "entries": [
3209                        {
3210                            "content": "Step 1",
3211                            "priority": "high",
3212                            "status": "pending"
3213                        }
3214                    ]
3215                }
3216            })
3217        );
3218    }
3219
3220    #[cfg(feature = "unstable_plan_operations")]
3221    #[test]
3222    fn test_plan_removed_serialization() {
3223        use serde_json::json;
3224
3225        assert_eq!(
3226            serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
3227            json!({
3228                "sessionUpdate": "plan_removed",
3229                "planId": "plan-1"
3230            })
3231        );
3232    }
3233
3234    #[test]
3235    fn available_command_input_preserves_unknown_typed_variant() {
3236        use serde_json::json;
3237
3238        let input: AvailableCommandInput = serde_json::from_value(json!({
3239            "type": "_choices",
3240            "hint": "Pick one",
3241            "options": ["fast", "careful"]
3242        }))
3243        .unwrap();
3244
3245        let AvailableCommandInput::Other(unknown) = input else {
3246            panic!("expected unknown command input");
3247        };
3248
3249        assert_eq!(unknown.type_, "_choices");
3250        assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
3251        assert_eq!(
3252            unknown.fields.get("options"),
3253            Some(&json!(["fast", "careful"]))
3254        );
3255        assert_eq!(
3256            serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
3257            json!({
3258                "type": "_choices",
3259                "hint": "Pick one",
3260                "options": ["fast", "careful"]
3261            })
3262        );
3263    }
3264
3265    #[test]
3266    fn available_command_input_text_uses_type_discriminator() {
3267        use serde_json::json;
3268
3269        let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
3270
3271        let json = serde_json::to_value(&input).unwrap();
3272        assert_eq!(
3273            json,
3274            json!({
3275                "type": "text",
3276                "hint": "Describe changes"
3277            })
3278        );
3279
3280        let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
3281        assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
3282    }
3283
3284    #[test]
3285    fn request_permission_subject_tool_call_uses_type_discriminator() {
3286        use serde_json::json;
3287
3288        let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
3289
3290        let json = serde_json::to_value(&subject).unwrap();
3291        assert_eq!(
3292            json,
3293            json!({
3294                "type": "tool_call",
3295                "toolCall": {
3296                    "toolCallId": "call_001"
3297                }
3298            })
3299        );
3300
3301        let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
3302        assert!(matches!(
3303            roundtripped,
3304            RequestPermissionSubject::ToolCall(_)
3305        ));
3306    }
3307
3308    #[test]
3309    fn request_permission_subject_command_uses_type_discriminator() {
3310        use serde_json::json;
3311
3312        let mut meta = Meta::new();
3313        meta.insert("source".to_string(), json!("shell"));
3314        let subject = RequestPermissionSubject::from(
3315            CommandPermissionSubject::new("cargo test", "/workspace/project")
3316                .tool_call_id("call_001")
3317                .terminal_id("term_1")
3318                .meta(meta),
3319        );
3320
3321        let json = serde_json::to_value(&subject).unwrap();
3322        assert_eq!(
3323            json,
3324            json!({
3325                "type": "command",
3326                "command": "cargo test",
3327                "cwd": "/workspace/project",
3328                "toolCallId": "call_001",
3329                "terminalId": "term_1",
3330                "_meta": {
3331                    "source": "shell"
3332                }
3333            })
3334        );
3335
3336        let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
3337        assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
3338    }
3339
3340    #[test]
3341    fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
3342        use serde_json::json;
3343
3344        let subject: RequestPermissionSubject = serde_json::from_value(json!({
3345            "type": "command",
3346            "command": "cargo test",
3347            "cwd": "/workspace/project",
3348            "toolCallId": null,
3349            "terminalId": null,
3350            "_meta": null
3351        }))
3352        .unwrap();
3353
3354        let RequestPermissionSubject::Command(subject) = subject else {
3355            panic!("expected command permission subject");
3356        };
3357        assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
3358        assert_eq!(subject.tool_call_id, None);
3359        assert_eq!(subject.terminal_id, None);
3360        assert_eq!(subject.meta, None);
3361        assert_eq!(
3362            serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
3363            json!({
3364                "type": "command",
3365                "command": "cargo test",
3366                "cwd": "/workspace/project"
3367            })
3368        );
3369    }
3370
3371    #[test]
3372    fn request_permission_subject_preserves_unknown_variant() {
3373        use serde_json::json;
3374
3375        let subject: RequestPermissionSubject = serde_json::from_value(json!({
3376            "type": "_review",
3377            "reason": "needs-review",
3378            "retryAfterSeconds": 30
3379        }))
3380        .unwrap();
3381
3382        let RequestPermissionSubject::Other(unknown) = subject else {
3383            panic!("expected unknown permission subject");
3384        };
3385
3386        assert_eq!(unknown.type_, "_review");
3387        assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
3388        assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
3389        assert_eq!(
3390            serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
3391            json!({
3392                "type": "_review",
3393                "reason": "needs-review",
3394                "retryAfterSeconds": 30
3395            })
3396        );
3397    }
3398
3399    #[test]
3400    fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
3401        use serde_json::json;
3402
3403        assert!(
3404            serde_json::from_value::<RequestPermissionSubject>(json!({
3405                "type": "tool_call"
3406            }))
3407            .is_err()
3408        );
3409        assert!(
3410            serde_json::from_value::<RequestPermissionSubject>(json!({
3411                "type": 1
3412            }))
3413            .is_err()
3414        );
3415        assert!(
3416            serde_json::from_value::<RequestPermissionSubject>(json!({
3417                "type": "command",
3418                "cwd": "/workspace/project"
3419            }))
3420            .is_err()
3421        );
3422        assert!(
3423            serde_json::from_value::<RequestPermissionSubject>(json!({
3424                "type": "command",
3425                "command": "cargo test"
3426            }))
3427            .is_err()
3428        );
3429        assert!(
3430            serde_json::from_value::<RequestPermissionSubject>(json!({
3431                "type": "command",
3432                "command": "cargo test",
3433                "cwd": null
3434            }))
3435            .is_err()
3436        );
3437    }
3438
3439    #[test]
3440    fn request_permission_title_and_description_are_separate_from_tool_call_content() {
3441        use serde_json::json;
3442
3443        let request =
3444            RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
3445                .description("Allow this tool to edit src/main.rs?")
3446                .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
3447                    "call_001",
3448                )));
3449
3450        assert_eq!(
3451            serde_json::to_value(request).unwrap(),
3452            json!({
3453                "sessionId": "sess_abc123def456",
3454                "title": "Approve file edit?",
3455                "description": "Allow this tool to edit src/main.rs?",
3456                "subject": {
3457                    "type": "tool_call",
3458                    "toolCall": {
3459                        "toolCallId": "call_001"
3460                    }
3461                },
3462                "options": []
3463            })
3464        );
3465    }
3466
3467    #[test]
3468    fn request_permission_requires_title_and_allows_missing_subject() {
3469        use serde_json::json;
3470
3471        let request = RequestPermissionRequest::new(
3472            "sess_abc123def456",
3473            "Approve elevated permissions?",
3474            Vec::new(),
3475        );
3476
3477        assert_eq!(
3478            serde_json::to_value(request).unwrap(),
3479            json!({
3480                "sessionId": "sess_abc123def456",
3481                "title": "Approve elevated permissions?",
3482                "options": []
3483            })
3484        );
3485
3486        let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
3487            "sessionId": "sess_abc123def456",
3488            "title": "Approve elevated permissions?",
3489            "options": []
3490        }))
3491        .unwrap();
3492        assert!(missing_subject.subject.is_none());
3493
3494        let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
3495            "sessionId": "sess_abc123def456",
3496            "title": "Approve elevated permissions?",
3497            "subject": null,
3498            "options": []
3499        }))
3500        .unwrap();
3501        assert!(null_subject.subject.is_none());
3502
3503        assert!(
3504            serde_json::from_value::<RequestPermissionRequest>(json!({
3505                "sessionId": "sess_abc123def456",
3506                "options": []
3507            }))
3508            .is_err()
3509        );
3510    }
3511
3512    #[test]
3513    fn request_permission_outcome_preserves_unknown_variant() {
3514        use serde_json::json;
3515
3516        let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
3517            "outcome": "_defer",
3518            "reason": "needs-review",
3519            "retryAfterSeconds": 30
3520        }))
3521        .unwrap();
3522
3523        let RequestPermissionOutcome::Other(unknown) = outcome else {
3524            panic!("expected unknown permission outcome");
3525        };
3526
3527        assert_eq!(unknown.outcome, "_defer");
3528        assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
3529        assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
3530        assert_eq!(
3531            serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
3532            json!({
3533                "outcome": "_defer",
3534                "reason": "needs-review",
3535                "retryAfterSeconds": 30
3536            })
3537        );
3538    }
3539
3540    #[test]
3541    fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
3542        use serde_json::json;
3543
3544        assert!(
3545            serde_json::from_value::<RequestPermissionOutcome>(json!({
3546                "outcome": "selected"
3547            }))
3548            .is_err()
3549        );
3550        assert!(
3551            serde_json::from_value::<RequestPermissionOutcome>(json!({
3552                "outcome": 1
3553            }))
3554            .is_err()
3555        );
3556    }
3557
3558    #[test]
3559    fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
3560        use serde_json::json;
3561
3562        assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
3563        assert!(
3564            serde_json::from_value::<AvailableCommandInput>(json!({
3565                "hint": "Pick one"
3566            }))
3567            .is_err()
3568        );
3569        assert!(
3570            serde_json::from_value::<AvailableCommandInput>(json!({
3571                "type": 1,
3572                "hint": "Pick one"
3573            }))
3574            .is_err()
3575        );
3576        assert!(
3577            serde_json::from_value::<OtherAvailableCommandInput>(json!({
3578                "type": "text",
3579                "hint": "Pick one"
3580            }))
3581            .is_err()
3582        );
3583    }
3584
3585    #[cfg(feature = "unstable_nes")]
3586    #[test]
3587    fn test_client_capabilities_position_encodings_serialization() {
3588        use serde_json::json;
3589
3590        let capabilities = ClientCapabilities::new().position_encodings(vec![
3591            PositionEncodingKind::Utf32,
3592            PositionEncodingKind::Utf16,
3593        ]);
3594        let json = serde_json::to_value(&capabilities).unwrap();
3595
3596        assert_eq!(json["positionEncodings"], json!(["utf-32", "utf-16"]));
3597    }
3598
3599    #[cfg(feature = "unstable_mcp_over_acp")]
3600    #[test]
3601    fn test_agent_mcp_request_method_names() {
3602        use serde_json::json;
3603
3604        let params: serde_json::Map<String, serde_json::Value> =
3605            [("cursor".to_string(), json!("abc"))].into_iter().collect();
3606
3607        assert_eq!(CLIENT_METHOD_NAMES.mcp_connect, "mcp/connect");
3608        assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
3609        assert_eq!(CLIENT_METHOD_NAMES.mcp_disconnect, "mcp/disconnect");
3610
3611        assert_eq!(
3612            AgentRequest::ConnectMcpRequest(Box::new(ConnectMcpRequest::new("server-1"))).method(),
3613            "mcp/connect"
3614        );
3615        assert_eq!(
3616            AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
3617                "conn-1",
3618                "tools/list"
3619            )))
3620            .method(),
3621            "mcp/message"
3622        );
3623        assert_eq!(
3624            AgentRequest::DisconnectMcpRequest(Box::new(DisconnectMcpRequest::new("conn-1")))
3625                .method(),
3626            "mcp/disconnect"
3627        );
3628        assert_eq!(
3629            AgentNotification::MessageMcpNotification(Box::new(MessageMcpNotification::new(
3630                "conn-1",
3631                "notifications/progress"
3632            )))
3633            .method(),
3634            "mcp/message"
3635        );
3636
3637        assert_eq!(
3638            serde_json::to_value(ConnectMcpRequest::new("server-1")).unwrap(),
3639            json!({ "serverId": "server-1" })
3640        );
3641        assert_eq!(
3642            serde_json::to_value(ConnectMcpResponse::new("conn-1")).unwrap(),
3643            json!({ "connectionId": "conn-1" })
3644        );
3645        assert_eq!(
3646            serde_json::to_value(MessageMcpRequest::new("conn-1", "tools/list").params(params))
3647                .unwrap(),
3648            json!({
3649                "connectionId": "conn-1",
3650                "method": "tools/list",
3651                "params": { "cursor": "abc" }
3652            })
3653        );
3654        assert_eq!(
3655            serde_json::to_value(DisconnectMcpRequest::new("conn-1")).unwrap(),
3656            json!({ "connectionId": "conn-1" })
3657        );
3658        assert_eq!(
3659            serde_json::to_value(MessageMcpNotification::new(
3660                "conn-1",
3661                "notifications/progress"
3662            ))
3663            .unwrap(),
3664            json!({
3665                "connectionId": "conn-1",
3666                "method": "notifications/progress"
3667            })
3668        );
3669
3670        let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
3671            "connectionId": "conn-1",
3672            "method": "tools/list",
3673            "params": null
3674        }))
3675        .unwrap();
3676        assert_eq!(request_with_null_params.params, None);
3677    }
3678
3679    #[test]
3680    fn test_auth_capabilities_serialize_terminal_support_as_object() {
3681        use serde_json::json;
3682
3683        let capabilities = AuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
3684
3685        assert_eq!(
3686            serde_json::to_value(&capabilities).unwrap(),
3687            json!({
3688                "terminal": {}
3689            })
3690        );
3691
3692        let deserialized: AuthCapabilities = serde_json::from_value(json!({
3693            "terminal": false
3694        }))
3695        .unwrap();
3696        assert!(deserialized.terminal.is_none());
3697    }
3698
3699    #[test]
3700    fn request_permission_request_rejects_malformed_options() {
3701        use serde_json::json;
3702
3703        assert!(
3704            serde_json::from_value::<RequestPermissionRequest>(json!({
3705                "sessionId": "sess-1",
3706                "title": "Run tool?",
3707                "options": "not-an-array"
3708            }))
3709            .is_err()
3710        );
3711        assert!(
3712            serde_json::from_value::<RequestPermissionRequest>(json!({
3713                "sessionId": "sess-1",
3714                "title": "Run tool?",
3715                "options": [{"optionId": "allow"}]
3716            }))
3717            .is_err()
3718        );
3719    }
3720
3721    #[cfg(feature = "unstable_plan_operations")]
3722    #[test]
3723    fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
3724        use serde_json::json;
3725
3726        assert!(
3727            serde_json::from_value::<SessionUpdate>(json!({
3728                "sessionUpdate": "plan_removed"
3729            }))
3730            .is_err()
3731        );
3732    }
3733}