Skip to main content

agent_client_protocol_schema/v1/
elicitation.rs

1//! Elicitation types for structured user input.
2//!
3//! **UNSTABLE**: This module is not part of the spec yet, and may be removed or changed at any point.
4//!
5//! This module defines the types used for agent-initiated elicitation,
6//! where the agent requests structured input from the user via forms or URLs.
7
8use std::{collections::BTreeMap, sync::Arc};
9
10use derive_more::{Display, From};
11use schemars::{JsonSchema, Schema};
12use serde::{Deserialize, Serialize};
13use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
14
15use crate::IntoOption;
16use crate::SkipListener;
17
18use super::{
19    ELICITATION_COMPLETE_NOTIFICATION, ELICITATION_CREATE_METHOD_NAME, Meta, RequestId, SessionId,
20    ToolCallId,
21};
22
23/// **UNSTABLE**
24///
25/// This capability is not part of the spec yet, and may be removed or changed at any point.
26///
27/// Unique identifier for an elicitation.
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
29#[serde(transparent)]
30#[from(Arc<str>, String, &'static str)]
31#[non_exhaustive]
32pub struct ElicitationId(pub Arc<str>);
33
34impl ElicitationId {
35    /// Wraps a protocol string as a typed [`ElicitationId`].
36    #[must_use]
37    pub fn new(id: impl Into<Arc<str>>) -> Self {
38        Self(id.into())
39    }
40}
41
42/// String format types for string properties in elicitation schemas.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
44#[serde(rename_all = "kebab-case")]
45#[non_exhaustive]
46pub enum StringFormat {
47    /// Email address format.
48    Email,
49    /// URI format.
50    Uri,
51    /// Date format (YYYY-MM-DD).
52    Date,
53    /// Date-time format (ISO 8601).
54    DateTime,
55}
56
57/// Type discriminator for elicitation schemas.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
59#[serde(rename_all = "snake_case")]
60#[non_exhaustive]
61pub enum ElicitationSchemaType {
62    /// Object schema type.
63    #[default]
64    Object,
65}
66
67/// A titled enum option with a const value, human-readable title, and optional description.
68#[serde_as]
69#[skip_serializing_none]
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71#[non_exhaustive]
72pub struct EnumOption {
73    /// The constant value for this option.
74    #[serde(rename = "const")]
75    pub value: String,
76    /// Human-readable title for this option.
77    pub title: String,
78    /// Human-readable description.
79    #[serde_as(deserialize_as = "DefaultOnError")]
80    #[schemars(extend("x-deserialize-default-on-error" = true))]
81    #[serde(default)]
82    pub description: Option<String>,
83    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
84    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
85    /// these keys.
86    ///
87    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
88    #[serde_as(deserialize_as = "DefaultOnError")]
89    #[schemars(extend("x-deserialize-default-on-error" = true))]
90    #[serde(default)]
91    #[serde(rename = "_meta")]
92    pub meta: Option<Meta>,
93}
94
95impl EnumOption {
96    /// Create a new enum option.
97    #[must_use]
98    pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
99        Self {
100            value: value.into(),
101            title: title.into(),
102            description: None,
103            meta: None,
104        }
105    }
106
107    /// Human-readable description.
108    #[must_use]
109    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
110        self.description = description.into_option();
111        self
112    }
113
114    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
115    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
116    /// these keys.
117    ///
118    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
119    #[must_use]
120    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
121        self.meta = meta.into_option();
122        self
123    }
124}
125
126/// Schema for string properties in an elicitation form.
127///
128/// When `enum` or `oneOf` is set, this represents a single-select enum
129/// with `"type": "string"`.
130#[serde_as]
131#[skip_serializing_none]
132#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
133#[serde(rename_all = "camelCase")]
134#[non_exhaustive]
135pub struct StringPropertySchema {
136    /// Optional title for the property.
137    #[serde_as(deserialize_as = "DefaultOnError")]
138    #[schemars(extend("x-deserialize-default-on-error" = true))]
139    #[serde(default)]
140    pub title: Option<String>,
141    /// Human-readable description.
142    #[serde_as(deserialize_as = "DefaultOnError")]
143    #[schemars(extend("x-deserialize-default-on-error" = true))]
144    #[serde(default)]
145    pub description: Option<String>,
146    /// Minimum string length.
147    #[serde(default)]
148    pub min_length: Option<u32>,
149    /// Maximum string length.
150    #[serde(default)]
151    pub max_length: Option<u32>,
152    /// Pattern the string must match.
153    #[serde(default)]
154    pub pattern: Option<String>,
155    /// String format.
156    #[serde(default)]
157    pub format: Option<StringFormat>,
158    /// Default value.
159    #[serde_as(deserialize_as = "DefaultOnError")]
160    #[schemars(extend("x-deserialize-default-on-error" = true))]
161    #[serde(default)]
162    pub default: Option<String>,
163    /// Enum values for untitled single-select enums.
164    #[serde(default)]
165    #[serde(rename = "enum")]
166    pub enum_values: Option<Vec<String>>,
167    /// Titled enum options for titled single-select enums.
168    #[serde(default)]
169    #[serde(rename = "oneOf")]
170    pub one_of: Option<Vec<EnumOption>>,
171    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
172    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
173    /// these keys.
174    ///
175    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
176    #[serde_as(deserialize_as = "DefaultOnError")]
177    #[schemars(extend("x-deserialize-default-on-error" = true))]
178    #[serde(default)]
179    #[serde(rename = "_meta")]
180    pub meta: Option<Meta>,
181}
182
183impl StringPropertySchema {
184    /// Create a new string property schema.
185    #[must_use]
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Create an email string property schema.
191    #[must_use]
192    pub fn email() -> Self {
193        Self {
194            format: Some(StringFormat::Email),
195            ..Default::default()
196        }
197    }
198
199    /// Create a URI string property schema.
200    #[must_use]
201    pub fn uri() -> Self {
202        Self {
203            format: Some(StringFormat::Uri),
204            ..Default::default()
205        }
206    }
207
208    /// Create a date string property schema.
209    #[must_use]
210    pub fn date() -> Self {
211        Self {
212            format: Some(StringFormat::Date),
213            ..Default::default()
214        }
215    }
216
217    /// Create a date-time string property schema.
218    #[must_use]
219    pub fn date_time() -> Self {
220        Self {
221            format: Some(StringFormat::DateTime),
222            ..Default::default()
223        }
224    }
225
226    /// Optional title for the property.
227    #[must_use]
228    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
229        self.title = title.into_option();
230        self
231    }
232
233    /// Human-readable description.
234    #[must_use]
235    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
236        self.description = description.into_option();
237        self
238    }
239
240    /// Minimum string length.
241    #[must_use]
242    pub fn min_length(mut self, min_length: impl IntoOption<u32>) -> Self {
243        self.min_length = min_length.into_option();
244        self
245    }
246
247    /// Maximum string length.
248    #[must_use]
249    pub fn max_length(mut self, max_length: impl IntoOption<u32>) -> Self {
250        self.max_length = max_length.into_option();
251        self
252    }
253
254    /// Pattern the string must match.
255    #[must_use]
256    pub fn pattern(mut self, pattern: impl IntoOption<String>) -> Self {
257        self.pattern = pattern.into_option();
258        self
259    }
260
261    /// String format.
262    #[must_use]
263    pub fn format(mut self, format: impl IntoOption<StringFormat>) -> Self {
264        self.format = format.into_option();
265        self
266    }
267
268    /// Default value.
269    #[must_use]
270    pub fn default_value(mut self, default: impl IntoOption<String>) -> Self {
271        self.default = default.into_option();
272        self
273    }
274
275    /// Enum values for untitled single-select enums.
276    #[must_use]
277    pub fn enum_values(mut self, enum_values: impl IntoOption<Vec<String>>) -> Self {
278        self.enum_values = enum_values.into_option();
279        self
280    }
281
282    /// Titled enum options for titled single-select enums.
283    #[must_use]
284    pub fn one_of(mut self, one_of: impl IntoOption<Vec<EnumOption>>) -> Self {
285        self.one_of = one_of.into_option();
286        self
287    }
288
289    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
290    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
291    /// these keys.
292    ///
293    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
294    #[must_use]
295    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
296        self.meta = meta.into_option();
297        self
298    }
299}
300
301/// Schema for number (floating-point) properties in an elicitation form.
302#[serde_as]
303#[skip_serializing_none]
304#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
305#[serde(rename_all = "camelCase")]
306#[non_exhaustive]
307pub struct NumberPropertySchema {
308    /// Optional title for the property.
309    #[serde_as(deserialize_as = "DefaultOnError")]
310    #[schemars(extend("x-deserialize-default-on-error" = true))]
311    #[serde(default)]
312    pub title: Option<String>,
313    /// Human-readable description.
314    #[serde_as(deserialize_as = "DefaultOnError")]
315    #[schemars(extend("x-deserialize-default-on-error" = true))]
316    #[serde(default)]
317    pub description: Option<String>,
318    /// Minimum value (inclusive).
319    #[serde(default)]
320    pub minimum: Option<f64>,
321    /// Maximum value (inclusive).
322    #[serde(default)]
323    pub maximum: Option<f64>,
324    /// Default value.
325    #[serde_as(deserialize_as = "DefaultOnError")]
326    #[schemars(extend("x-deserialize-default-on-error" = true))]
327    #[serde(default)]
328    pub default: Option<f64>,
329    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
330    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
331    /// these keys.
332    ///
333    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
334    #[serde_as(deserialize_as = "DefaultOnError")]
335    #[schemars(extend("x-deserialize-default-on-error" = true))]
336    #[serde(default)]
337    #[serde(rename = "_meta")]
338    pub meta: Option<Meta>,
339}
340
341impl NumberPropertySchema {
342    /// Create a new number property schema.
343    #[must_use]
344    pub fn new() -> Self {
345        Self::default()
346    }
347
348    /// Optional title for the property.
349    #[must_use]
350    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
351        self.title = title.into_option();
352        self
353    }
354
355    /// Human-readable description.
356    #[must_use]
357    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
358        self.description = description.into_option();
359        self
360    }
361
362    /// Minimum value (inclusive).
363    #[must_use]
364    pub fn minimum(mut self, minimum: impl IntoOption<f64>) -> Self {
365        self.minimum = minimum.into_option();
366        self
367    }
368
369    /// Maximum value (inclusive).
370    #[must_use]
371    pub fn maximum(mut self, maximum: impl IntoOption<f64>) -> Self {
372        self.maximum = maximum.into_option();
373        self
374    }
375
376    /// Default value.
377    #[must_use]
378    pub fn default_value(mut self, default: impl IntoOption<f64>) -> Self {
379        self.default = default.into_option();
380        self
381    }
382
383    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
384    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
385    /// these keys.
386    ///
387    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
388    #[must_use]
389    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
390        self.meta = meta.into_option();
391        self
392    }
393}
394
395/// Schema for integer properties in an elicitation form.
396#[serde_as]
397#[skip_serializing_none]
398#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
399#[serde(rename_all = "camelCase")]
400#[non_exhaustive]
401pub struct IntegerPropertySchema {
402    /// Optional title for the property.
403    #[serde_as(deserialize_as = "DefaultOnError")]
404    #[schemars(extend("x-deserialize-default-on-error" = true))]
405    #[serde(default)]
406    pub title: Option<String>,
407    /// Human-readable description.
408    #[serde_as(deserialize_as = "DefaultOnError")]
409    #[schemars(extend("x-deserialize-default-on-error" = true))]
410    #[serde(default)]
411    pub description: Option<String>,
412    /// Minimum value (inclusive).
413    #[serde(default)]
414    pub minimum: Option<i64>,
415    /// Maximum value (inclusive).
416    #[serde(default)]
417    pub maximum: Option<i64>,
418    /// Default value.
419    #[serde_as(deserialize_as = "DefaultOnError")]
420    #[schemars(extend("x-deserialize-default-on-error" = true))]
421    #[serde(default)]
422    pub default: Option<i64>,
423    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
424    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
425    /// these keys.
426    ///
427    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
428    #[serde_as(deserialize_as = "DefaultOnError")]
429    #[schemars(extend("x-deserialize-default-on-error" = true))]
430    #[serde(default)]
431    #[serde(rename = "_meta")]
432    pub meta: Option<Meta>,
433}
434
435impl IntegerPropertySchema {
436    /// Create a new integer property schema.
437    #[must_use]
438    pub fn new() -> Self {
439        Self::default()
440    }
441
442    /// Optional title for the property.
443    #[must_use]
444    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
445        self.title = title.into_option();
446        self
447    }
448
449    /// Human-readable description.
450    #[must_use]
451    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
452        self.description = description.into_option();
453        self
454    }
455
456    /// Minimum value (inclusive).
457    #[must_use]
458    pub fn minimum(mut self, minimum: impl IntoOption<i64>) -> Self {
459        self.minimum = minimum.into_option();
460        self
461    }
462
463    /// Maximum value (inclusive).
464    #[must_use]
465    pub fn maximum(mut self, maximum: impl IntoOption<i64>) -> Self {
466        self.maximum = maximum.into_option();
467        self
468    }
469
470    /// Default value.
471    #[must_use]
472    pub fn default_value(mut self, default: impl IntoOption<i64>) -> Self {
473        self.default = default.into_option();
474        self
475    }
476
477    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
478    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
479    /// these keys.
480    ///
481    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
482    #[must_use]
483    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
484        self.meta = meta.into_option();
485        self
486    }
487}
488
489/// Schema for boolean properties in an elicitation form.
490#[serde_as]
491#[skip_serializing_none]
492#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
493#[serde(rename_all = "camelCase")]
494#[non_exhaustive]
495pub struct BooleanPropertySchema {
496    /// Optional title for the property.
497    #[serde_as(deserialize_as = "DefaultOnError")]
498    #[schemars(extend("x-deserialize-default-on-error" = true))]
499    #[serde(default)]
500    pub title: Option<String>,
501    /// Human-readable description.
502    #[serde_as(deserialize_as = "DefaultOnError")]
503    #[schemars(extend("x-deserialize-default-on-error" = true))]
504    #[serde(default)]
505    pub description: Option<String>,
506    /// Default value.
507    #[serde_as(deserialize_as = "DefaultOnError")]
508    #[schemars(extend("x-deserialize-default-on-error" = true))]
509    #[serde(default)]
510    pub default: Option<bool>,
511    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
512    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
513    /// these keys.
514    ///
515    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
516    #[serde_as(deserialize_as = "DefaultOnError")]
517    #[schemars(extend("x-deserialize-default-on-error" = true))]
518    #[serde(default)]
519    #[serde(rename = "_meta")]
520    pub meta: Option<Meta>,
521}
522
523impl BooleanPropertySchema {
524    /// Create a new boolean property schema.
525    #[must_use]
526    pub fn new() -> Self {
527        Self::default()
528    }
529
530    /// Optional title for the property.
531    #[must_use]
532    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
533        self.title = title.into_option();
534        self
535    }
536
537    /// Human-readable description.
538    #[must_use]
539    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
540        self.description = description.into_option();
541        self
542    }
543
544    /// Default value.
545    #[must_use]
546    pub fn default_value(mut self, default: impl IntoOption<bool>) -> Self {
547        self.default = default.into_option();
548        self
549    }
550
551    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
552    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
553    /// these keys.
554    ///
555    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
556    #[must_use]
557    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
558        self.meta = meta.into_option();
559        self
560    }
561}
562
563/// String item schema for multi-select enum properties.
564#[serde_as]
565#[skip_serializing_none]
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
567#[non_exhaustive]
568pub struct StringMultiSelectItems {
569    /// Allowed enum values.
570    #[serde(rename = "enum")]
571    pub values: Vec<String>,
572    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
573    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
574    /// these keys.
575    ///
576    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
577    #[serde_as(deserialize_as = "DefaultOnError")]
578    #[schemars(extend("x-deserialize-default-on-error" = true))]
579    #[serde(default)]
580    #[serde(rename = "_meta")]
581    pub meta: Option<Meta>,
582}
583
584impl StringMultiSelectItems {
585    /// Create new string multi-select items.
586    #[must_use]
587    pub fn new(values: Vec<String>) -> Self {
588        Self { values, meta: None }
589    }
590
591    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
592    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
593    /// these keys.
594    ///
595    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
596    #[must_use]
597    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
598        self.meta = meta.into_option();
599        self
600    }
601}
602
603/// Items definition for titled multi-select enum properties.
604#[serde_as]
605#[skip_serializing_none]
606#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
607#[non_exhaustive]
608pub struct TitledMultiSelectItems {
609    /// Titled enum options.
610    #[serde(rename = "anyOf")]
611    pub options: Vec<EnumOption>,
612    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
613    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
614    /// these keys.
615    ///
616    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
617    #[serde_as(deserialize_as = "DefaultOnError")]
618    #[schemars(extend("x-deserialize-default-on-error" = true))]
619    #[serde(default)]
620    #[serde(rename = "_meta")]
621    pub meta: Option<Meta>,
622}
623
624impl TitledMultiSelectItems {
625    /// Create new titled multi-select items.
626    #[must_use]
627    pub fn new(options: Vec<EnumOption>) -> Self {
628        Self {
629            options,
630            meta: None,
631        }
632    }
633
634    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
635    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
636    /// these keys.
637    ///
638    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
639    #[must_use]
640    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
641        self.meta = meta.into_option();
642        self
643    }
644}
645
646/// Custom or future typed item schema for multi-select properties.
647///
648/// This preserves unknown item `type` values and the rest of the `items`
649/// payload for clients that store, replay, proxy, or forward elicitation
650/// requests.
651#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
652#[schemars(inline)]
653#[schemars(transform = other_multi_select_items_schema)]
654#[serde(rename_all = "camelCase")]
655#[non_exhaustive]
656pub struct OtherMultiSelectItems {
657    /// Custom or future multi-select item type.
658    ///
659    /// Values beginning with `_` are reserved for implementation-specific
660    /// extensions. Unknown values that do not begin with `_` are reserved for
661    /// future ACP variants.
662    #[serde(rename = "type")]
663    pub type_: String,
664    /// Additional fields from the unknown item schema payload.
665    #[serde(flatten)]
666    pub fields: BTreeMap<String, serde_json::Value>,
667}
668
669impl OtherMultiSelectItems {
670    /// Builds [`OtherMultiSelectItems`] from an unknown discriminator and preserves the remaining extension fields.
671    #[must_use]
672    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
673        fields.remove("type");
674        Self {
675            type_: type_.into(),
676            fields,
677        }
678    }
679}
680
681impl<'de> Deserialize<'de> for OtherMultiSelectItems {
682    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
683    where
684        D: serde::Deserializer<'de>,
685    {
686        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
687        let type_ = fields
688            .remove("type")
689            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
690        let serde_json::Value::String(type_) = type_ else {
691            return Err(serde::de::Error::custom("`type` must be a string"));
692        };
693
694        if is_known_multi_select_item_type(&type_) {
695            return Err(serde::de::Error::custom(format!(
696                "known multi-select item type `{type_}` did not match its schema"
697            )));
698        }
699
700        Ok(Self { type_, fields })
701    }
702}
703
704const KNOWN_MULTI_SELECT_ITEM_TYPES: &[&str] = &["string"];
705
706fn is_known_multi_select_item_type(type_: &str) -> bool {
707    KNOWN_MULTI_SELECT_ITEM_TYPES.contains(&type_)
708}
709
710fn other_multi_select_items_schema(schema: &mut Schema) {
711    schema.insert(
712        "not".into(),
713        serde_json::json!({
714            "anyOf": [
715                {
716                    "properties": {
717                        "type": {
718                            "const": "string",
719                            "type": "string"
720                        }
721                    },
722                    "required": ["type"],
723                    "type": "object"
724                }
725            ]
726        }),
727    );
728}
729
730/// Items for a multi-select (array) property schema.
731#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
732#[serde(tag = "type", rename_all = "snake_case")]
733#[non_exhaustive]
734pub enum MultiSelectItems {
735    /// Multi-select string items with plain string values.
736    String(StringMultiSelectItems),
737    /// Custom or future typed multi-select items.
738    #[serde(untagged)]
739    Other(OtherMultiSelectItems),
740    /// Titled multi-select items with human-readable labels.
741    #[serde(untagged)]
742    Titled(TitledMultiSelectItems),
743}
744
745/// Schema for multi-select (array) properties in an elicitation form.
746#[serde_as]
747#[skip_serializing_none]
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
749#[serde(rename_all = "camelCase")]
750#[non_exhaustive]
751pub struct MultiSelectPropertySchema {
752    /// Optional title for the property.
753    #[serde_as(deserialize_as = "DefaultOnError")]
754    #[schemars(extend("x-deserialize-default-on-error" = true))]
755    #[serde(default)]
756    pub title: Option<String>,
757    /// Human-readable description.
758    #[serde_as(deserialize_as = "DefaultOnError")]
759    #[schemars(extend("x-deserialize-default-on-error" = true))]
760    #[serde(default)]
761    pub description: Option<String>,
762    /// Minimum number of items to select.
763    #[serde(default)]
764    pub min_items: Option<u64>,
765    /// Maximum number of items to select.
766    #[serde(default)]
767    pub max_items: Option<u64>,
768    /// The items definition describing allowed values.
769    pub items: MultiSelectItems,
770    /// Default selected values.
771    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
772    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
773    #[serde(default)]
774    pub default: Option<Vec<String>>,
775    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
776    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
777    /// these keys.
778    ///
779    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
780    #[serde_as(deserialize_as = "DefaultOnError")]
781    #[schemars(extend("x-deserialize-default-on-error" = true))]
782    #[serde(default)]
783    #[serde(rename = "_meta")]
784    pub meta: Option<Meta>,
785}
786
787impl MultiSelectPropertySchema {
788    /// Create a new untitled multi-select property schema.
789    #[must_use]
790    pub fn new(values: Vec<String>) -> Self {
791        Self {
792            title: None,
793            description: None,
794            min_items: None,
795            max_items: None,
796            items: MultiSelectItems::String(StringMultiSelectItems::new(values)),
797            default: None,
798            meta: None,
799        }
800    }
801
802    /// Create a new titled multi-select property schema.
803    #[must_use]
804    pub fn titled(options: Vec<EnumOption>) -> Self {
805        Self {
806            title: None,
807            description: None,
808            min_items: None,
809            max_items: None,
810            items: MultiSelectItems::Titled(TitledMultiSelectItems::new(options)),
811            default: None,
812            meta: None,
813        }
814    }
815
816    /// Optional title for the property.
817    #[must_use]
818    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
819        self.title = title.into_option();
820        self
821    }
822
823    /// Human-readable description.
824    #[must_use]
825    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
826        self.description = description.into_option();
827        self
828    }
829
830    /// Minimum number of items to select.
831    #[must_use]
832    pub fn min_items(mut self, min_items: impl IntoOption<u64>) -> Self {
833        self.min_items = min_items.into_option();
834        self
835    }
836
837    /// Maximum number of items to select.
838    #[must_use]
839    pub fn max_items(mut self, max_items: impl IntoOption<u64>) -> Self {
840        self.max_items = max_items.into_option();
841        self
842    }
843
844    /// Default selected values.
845    #[must_use]
846    pub fn default_value(mut self, default: impl IntoOption<Vec<String>>) -> Self {
847        self.default = default.into_option();
848        self
849    }
850
851    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
852    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
853    /// these keys.
854    ///
855    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
856    #[must_use]
857    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
858        self.meta = meta.into_option();
859        self
860    }
861}
862
863/// Property schema for elicitation form fields.
864///
865/// Each variant corresponds to a JSON Schema `"type"` value.
866/// Single-select enums use the `String` variant with `enum` or `oneOf` set.
867/// Multi-select enums use the `Array` variant.
868#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
869#[serde(tag = "type", rename_all = "snake_case")]
870#[non_exhaustive]
871pub enum ElicitationPropertySchema {
872    /// String property (or single-select enum when `enum`/`oneOf` is set).
873    String(StringPropertySchema),
874    /// Number (floating-point) property.
875    Number(NumberPropertySchema),
876    /// Integer property.
877    Integer(IntegerPropertySchema),
878    /// Boolean property.
879    Boolean(BooleanPropertySchema),
880    /// Multi-select array property.
881    Array(MultiSelectPropertySchema),
882    /// Custom or future elicitation property schema.
883    ///
884    /// Values beginning with `_` are reserved for implementation-specific
885    /// extensions. Unknown values that do not begin with `_` are reserved for
886    /// future ACP variants.
887    ///
888    /// Clients that do not understand this property schema type should preserve
889    /// the raw schema when storing, replaying, proxying, or forwarding
890    /// elicitation requests. They MUST NOT render it as a known input control.
891    #[serde(untagged)]
892    Other(OtherElicitationPropertySchema),
893}
894
895/// Custom or future elicitation property schema payload.
896///
897/// This preserves the unknown `type` discriminator and the rest of the property
898/// schema object for clients that store, replay, proxy, or forward elicitation
899/// requests.
900#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
901#[schemars(inline)]
902#[schemars(transform = other_elicitation_property_schema_schema)]
903#[serde(rename_all = "camelCase")]
904#[non_exhaustive]
905pub struct OtherElicitationPropertySchema {
906    /// Custom or future elicitation property schema type.
907    ///
908    /// Values beginning with `_` are reserved for implementation-specific
909    /// extensions. Unknown values that do not begin with `_` are reserved for
910    /// future ACP variants.
911    #[serde(rename = "type")]
912    pub type_: String,
913    /// Additional fields from the unknown property schema payload.
914    #[serde(flatten)]
915    pub fields: BTreeMap<String, serde_json::Value>,
916}
917
918impl OtherElicitationPropertySchema {
919    /// Builds [`OtherElicitationPropertySchema`] from an unknown discriminator and preserves the remaining extension fields.
920    #[must_use]
921    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
922        fields.remove("type");
923        Self {
924            type_: type_.into(),
925            fields,
926        }
927    }
928}
929
930impl<'de> Deserialize<'de> for OtherElicitationPropertySchema {
931    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
932    where
933        D: serde::Deserializer<'de>,
934    {
935        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
936        let type_ = fields
937            .remove("type")
938            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
939        let serde_json::Value::String(type_) = type_ else {
940            return Err(serde::de::Error::custom("`type` must be a string"));
941        };
942
943        if is_known_elicitation_property_schema_type(&type_) {
944            return Err(serde::de::Error::custom(format!(
945                "known elicitation property schema type `{type_}` did not match its schema"
946            )));
947        }
948
949        Ok(Self { type_, fields })
950    }
951}
952
953const KNOWN_ELICITATION_PROPERTY_SCHEMA_TYPES: &[&str] =
954    &["string", "number", "integer", "boolean", "array"];
955
956fn is_known_elicitation_property_schema_type(type_: &str) -> bool {
957    KNOWN_ELICITATION_PROPERTY_SCHEMA_TYPES.contains(&type_)
958}
959
960fn other_elicitation_property_schema_schema(schema: &mut Schema) {
961    let known_value_schemas: Vec<_> = KNOWN_ELICITATION_PROPERTY_SCHEMA_TYPES
962        .iter()
963        .map(|value| {
964            serde_json::json!({
965                "properties": {
966                    "type": {
967                        "const": value,
968                        "type": "string"
969                    }
970                },
971                "required": ["type"],
972                "type": "object"
973            })
974        })
975        .collect();
976
977    schema.insert(
978        "not".into(),
979        serde_json::json!({
980            "anyOf": known_value_schemas
981        }),
982    );
983}
984
985impl From<StringPropertySchema> for ElicitationPropertySchema {
986    fn from(schema: StringPropertySchema) -> Self {
987        Self::String(schema)
988    }
989}
990
991impl From<NumberPropertySchema> for ElicitationPropertySchema {
992    fn from(schema: NumberPropertySchema) -> Self {
993        Self::Number(schema)
994    }
995}
996
997impl From<IntegerPropertySchema> for ElicitationPropertySchema {
998    fn from(schema: IntegerPropertySchema) -> Self {
999        Self::Integer(schema)
1000    }
1001}
1002
1003impl From<BooleanPropertySchema> for ElicitationPropertySchema {
1004    fn from(schema: BooleanPropertySchema) -> Self {
1005        Self::Boolean(schema)
1006    }
1007}
1008
1009impl From<MultiSelectPropertySchema> for ElicitationPropertySchema {
1010    fn from(schema: MultiSelectPropertySchema) -> Self {
1011        Self::Array(schema)
1012    }
1013}
1014
1015fn default_object_type() -> ElicitationSchemaType {
1016    ElicitationSchemaType::Object
1017}
1018
1019/// Type-safe elicitation schema for requesting structured user input.
1020///
1021/// This represents a JSON Schema object with primitive-typed properties,
1022/// as required by the elicitation specification.
1023#[serde_as]
1024#[skip_serializing_none]
1025#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1026#[serde(rename_all = "camelCase")]
1027#[non_exhaustive]
1028pub struct ElicitationSchema {
1029    /// Type discriminator. Always `"object"`.
1030    #[serde_as(deserialize_as = "DefaultOnError")]
1031    #[schemars(extend("x-deserialize-default-on-error" = true))]
1032    #[serde(rename = "type", default = "default_object_type")]
1033    pub type_: ElicitationSchemaType,
1034    /// Optional title for the schema.
1035    #[serde_as(deserialize_as = "DefaultOnError")]
1036    #[schemars(extend("x-deserialize-default-on-error" = true))]
1037    #[serde(default)]
1038    pub title: Option<String>,
1039    /// Property definitions (must be primitive types).
1040    #[serde(default)]
1041    pub properties: BTreeMap<String, ElicitationPropertySchema>,
1042    /// List of required property names.
1043    #[serde(default)]
1044    pub required: Option<Vec<String>>,
1045    /// Optional description of what this schema represents.
1046    #[serde_as(deserialize_as = "DefaultOnError")]
1047    #[schemars(extend("x-deserialize-default-on-error" = true))]
1048    #[serde(default)]
1049    pub description: Option<String>,
1050    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1051    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1052    /// these keys.
1053    ///
1054    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1055    #[serde_as(deserialize_as = "DefaultOnError")]
1056    #[schemars(extend("x-deserialize-default-on-error" = true))]
1057    #[serde(default)]
1058    #[serde(rename = "_meta")]
1059    pub meta: Option<Meta>,
1060}
1061
1062impl Default for ElicitationSchema {
1063    fn default() -> Self {
1064        Self {
1065            type_: default_object_type(),
1066            title: None,
1067            properties: BTreeMap::new(),
1068            required: None,
1069            description: None,
1070            meta: None,
1071        }
1072    }
1073}
1074
1075impl ElicitationSchema {
1076    /// Create a new empty elicitation schema.
1077    #[must_use]
1078    pub fn new() -> Self {
1079        Self::default()
1080    }
1081
1082    /// Optional title for the schema.
1083    #[must_use]
1084    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
1085        self.title = title.into_option();
1086        self
1087    }
1088
1089    /// Optional description of what this schema represents.
1090    #[must_use]
1091    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
1092        self.description = description.into_option();
1093        self
1094    }
1095
1096    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1097    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1098    /// these keys.
1099    ///
1100    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1101    #[must_use]
1102    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1103        self.meta = meta.into_option();
1104        self
1105    }
1106
1107    /// Add a property to the schema.
1108    #[must_use]
1109    pub fn property<S>(mut self, name: impl Into<String>, schema: S, required: bool) -> Self
1110    where
1111        S: Into<ElicitationPropertySchema>,
1112    {
1113        let name = name.into();
1114        self.properties.insert(name.clone(), schema.into());
1115
1116        if required {
1117            let required_fields = self.required.get_or_insert_with(Vec::new);
1118            if !required_fields.contains(&name) {
1119                required_fields.push(name);
1120            }
1121        } else if let Some(required_fields) = &mut self.required {
1122            required_fields.retain(|field| field != &name);
1123
1124            if required_fields.is_empty() {
1125                self.required = None;
1126            }
1127        }
1128
1129        self
1130    }
1131
1132    /// Add a string property.
1133    #[must_use]
1134    pub fn string(self, name: impl Into<String>, required: bool) -> Self {
1135        self.property(name, StringPropertySchema::new(), required)
1136    }
1137
1138    /// Add an email property.
1139    #[must_use]
1140    pub fn email(self, name: impl Into<String>, required: bool) -> Self {
1141        self.property(name, StringPropertySchema::email(), required)
1142    }
1143
1144    /// Add a URI property.
1145    #[must_use]
1146    pub fn uri(self, name: impl Into<String>, required: bool) -> Self {
1147        self.property(name, StringPropertySchema::uri(), required)
1148    }
1149
1150    /// Add a date property.
1151    #[must_use]
1152    pub fn date(self, name: impl Into<String>, required: bool) -> Self {
1153        self.property(name, StringPropertySchema::date(), required)
1154    }
1155
1156    /// Add a date-time property.
1157    #[must_use]
1158    pub fn date_time(self, name: impl Into<String>, required: bool) -> Self {
1159        self.property(name, StringPropertySchema::date_time(), required)
1160    }
1161
1162    /// Add a number property with range.
1163    #[must_use]
1164    pub fn number(self, name: impl Into<String>, min: f64, max: f64, required: bool) -> Self {
1165        self.property(
1166            name,
1167            NumberPropertySchema::new().minimum(min).maximum(max),
1168            required,
1169        )
1170    }
1171
1172    /// Add an integer property with range.
1173    #[must_use]
1174    pub fn integer(self, name: impl Into<String>, min: i64, max: i64, required: bool) -> Self {
1175        self.property(
1176            name,
1177            IntegerPropertySchema::new().minimum(min).maximum(max),
1178            required,
1179        )
1180    }
1181
1182    /// Add a boolean property.
1183    #[must_use]
1184    pub fn boolean(self, name: impl Into<String>, required: bool) -> Self {
1185        self.property(name, BooleanPropertySchema::new(), required)
1186    }
1187}
1188
1189/// **UNSTABLE**
1190///
1191/// This capability is not part of the spec yet, and may be removed or changed at any point.
1192///
1193/// Elicitation capabilities supported by the client.
1194#[serde_as]
1195#[skip_serializing_none]
1196#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1197#[serde(rename_all = "camelCase")]
1198#[non_exhaustive]
1199pub struct ElicitationCapabilities {
1200    /// Whether the client supports form-based elicitation.
1201    ///
1202    /// Optional. Omitted or `null` both mean the client does not advertise support.
1203    /// Supplying `{}` means the client supports form-based elicitation.
1204    #[serde_as(deserialize_as = "DefaultOnError")]
1205    #[schemars(extend("x-deserialize-default-on-error" = true))]
1206    #[serde(default)]
1207    pub form: Option<ElicitationFormCapabilities>,
1208    /// Whether the client supports URL-based elicitation.
1209    ///
1210    /// Optional. Omitted or `null` both mean the client does not advertise support.
1211    /// Supplying `{}` means the client supports URL-based elicitation.
1212    #[serde_as(deserialize_as = "DefaultOnError")]
1213    #[schemars(extend("x-deserialize-default-on-error" = true))]
1214    #[serde(default)]
1215    pub url: Option<ElicitationUrlCapabilities>,
1216    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1217    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1218    /// these keys.
1219    ///
1220    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1221    #[serde_as(deserialize_as = "DefaultOnError")]
1222    #[schemars(extend("x-deserialize-default-on-error" = true))]
1223    #[serde(default)]
1224    #[serde(rename = "_meta")]
1225    pub meta: Option<Meta>,
1226}
1227
1228impl ElicitationCapabilities {
1229    /// Builds an empty [`ElicitationCapabilities`]; use builder methods to advertise supported sub-capabilities.
1230    #[must_use]
1231    pub fn new() -> Self {
1232        Self::default()
1233    }
1234
1235    /// Whether the client supports form-based elicitation.
1236    ///
1237    /// Omitted or `null` both mean the client does not advertise support.
1238    /// Supplying `{}` means the client supports form-based elicitation.
1239    #[must_use]
1240    pub fn form(mut self, form: impl IntoOption<ElicitationFormCapabilities>) -> Self {
1241        self.form = form.into_option();
1242        self
1243    }
1244
1245    /// Whether the client supports URL-based elicitation.
1246    ///
1247    /// Omitted or `null` both mean the client does not advertise support.
1248    /// Supplying `{}` means the client supports URL-based elicitation.
1249    #[must_use]
1250    pub fn url(mut self, url: impl IntoOption<ElicitationUrlCapabilities>) -> Self {
1251        self.url = url.into_option();
1252        self
1253    }
1254
1255    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1256    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1257    /// these keys.
1258    ///
1259    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1260    #[must_use]
1261    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1262        self.meta = meta.into_option();
1263        self
1264    }
1265}
1266
1267/// **UNSTABLE**
1268///
1269/// This capability is not part of the spec yet, and may be removed or changed at any point.
1270///
1271/// Form-based elicitation capabilities.
1272///
1273/// Supplying `{}` means the client supports form-based elicitation.
1274#[serde_as]
1275#[skip_serializing_none]
1276#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1277#[serde(rename_all = "camelCase")]
1278#[non_exhaustive]
1279pub struct ElicitationFormCapabilities {
1280    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1281    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1282    /// these keys.
1283    ///
1284    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1285    #[serde_as(deserialize_as = "DefaultOnError")]
1286    #[schemars(extend("x-deserialize-default-on-error" = true))]
1287    #[serde(default)]
1288    #[serde(rename = "_meta")]
1289    pub meta: Option<Meta>,
1290}
1291
1292impl ElicitationFormCapabilities {
1293    /// Builds an empty [`ElicitationFormCapabilities`]; use builder methods to advertise supported sub-capabilities.
1294    #[must_use]
1295    pub fn new() -> Self {
1296        Self::default()
1297    }
1298
1299    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1300    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1301    /// these keys.
1302    ///
1303    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1304    #[must_use]
1305    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1306        self.meta = meta.into_option();
1307        self
1308    }
1309}
1310
1311/// **UNSTABLE**
1312///
1313/// This capability is not part of the spec yet, and may be removed or changed at any point.
1314///
1315/// URL-based elicitation capabilities.
1316///
1317/// Supplying `{}` means the client supports URL-based elicitation.
1318#[serde_as]
1319#[skip_serializing_none]
1320#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1321#[serde(rename_all = "camelCase")]
1322#[non_exhaustive]
1323pub struct ElicitationUrlCapabilities {
1324    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1325    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1326    /// these keys.
1327    ///
1328    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1329    #[serde_as(deserialize_as = "DefaultOnError")]
1330    #[schemars(extend("x-deserialize-default-on-error" = true))]
1331    #[serde(default)]
1332    #[serde(rename = "_meta")]
1333    pub meta: Option<Meta>,
1334}
1335
1336impl ElicitationUrlCapabilities {
1337    /// Builds an empty [`ElicitationUrlCapabilities`]; use builder methods to advertise supported sub-capabilities.
1338    #[must_use]
1339    pub fn new() -> Self {
1340        Self::default()
1341    }
1342
1343    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1344    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1345    /// these keys.
1346    ///
1347    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1348    #[must_use]
1349    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1350        self.meta = meta.into_option();
1351        self
1352    }
1353}
1354
1355/// **UNSTABLE**
1356///
1357/// This capability is not part of the spec yet, and may be removed or changed at any point.
1358///
1359/// The scope of an elicitation request, determining what context it's tied to.
1360#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1361#[serde(untagged)]
1362#[non_exhaustive]
1363pub enum ElicitationScope {
1364    /// Tied to a session, optionally to a specific tool call within that session.
1365    Session(ElicitationSessionScope),
1366    /// Tied to a specific JSON-RPC request outside of a session
1367    /// (e.g., during auth/configuration phases before any session is started).
1368    Request(ElicitationRequestScope),
1369}
1370
1371/// **UNSTABLE**
1372///
1373/// This capability is not part of the spec yet, and may be removed or changed at any point.
1374///
1375/// Session-scoped elicitation, optionally tied to a specific tool call.
1376///
1377/// When `tool_call_id` is set, the elicitation is tied to a specific tool call.
1378/// This is useful when an agent receives an elicitation from an MCP server
1379/// during a tool call and needs to redirect it to the user.
1380#[serde_as]
1381#[skip_serializing_none]
1382#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1383#[serde(rename_all = "camelCase")]
1384#[non_exhaustive]
1385pub struct ElicitationSessionScope {
1386    /// The session this elicitation is tied to.
1387    pub session_id: SessionId,
1388    /// Optional tool call within the session.
1389    #[serde_as(deserialize_as = "DefaultOnError")]
1390    #[schemars(extend("x-deserialize-default-on-error" = true))]
1391    #[serde(default)]
1392    pub tool_call_id: Option<ToolCallId>,
1393}
1394
1395impl ElicitationSessionScope {
1396    /// Builds [`ElicitationSessionScope`] with the required fields set; optional fields start unset or empty.
1397    #[must_use]
1398    pub fn new(session_id: impl Into<SessionId>) -> Self {
1399        Self {
1400            session_id: session_id.into(),
1401            tool_call_id: None,
1402        }
1403    }
1404
1405    /// Sets or clears the optional `toolCallId` field.
1406    #[must_use]
1407    pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
1408        self.tool_call_id = tool_call_id.into_option();
1409        self
1410    }
1411}
1412
1413/// **UNSTABLE**
1414///
1415/// This capability is not part of the spec yet, and may be removed or changed at any point.
1416///
1417/// Request-scoped elicitation, tied to a specific JSON-RPC request outside of a session
1418/// (e.g., during auth/configuration phases before any session is started).
1419#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1420#[serde(rename_all = "camelCase")]
1421#[non_exhaustive]
1422pub struct ElicitationRequestScope {
1423    /// The request this elicitation is tied to.
1424    pub request_id: RequestId,
1425}
1426
1427impl ElicitationRequestScope {
1428    /// Builds [`ElicitationRequestScope`] with the required fields set; optional fields start unset or empty.
1429    #[must_use]
1430    pub fn new(request_id: impl Into<RequestId>) -> Self {
1431        Self {
1432            request_id: request_id.into(),
1433        }
1434    }
1435}
1436
1437impl From<ElicitationSessionScope> for ElicitationScope {
1438    fn from(scope: ElicitationSessionScope) -> Self {
1439        Self::Session(scope)
1440    }
1441}
1442
1443impl From<ElicitationRequestScope> for ElicitationScope {
1444    fn from(scope: ElicitationRequestScope) -> Self {
1445        Self::Request(scope)
1446    }
1447}
1448
1449/// **UNSTABLE**
1450///
1451/// This capability is not part of the spec yet, and may be removed or changed at any point.
1452///
1453/// Request from the agent to elicit structured user input.
1454///
1455/// The agent sends this to the client to request information from the user,
1456/// either via a form or by directing them to a URL.
1457/// Elicitations are tied to a session (optionally a tool call) or a request.
1458#[serde_as]
1459#[skip_serializing_none]
1460#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1461#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1462#[serde(rename_all = "camelCase")]
1463#[non_exhaustive]
1464pub struct CreateElicitationRequest {
1465    /// The elicitation mode and its mode-specific fields.
1466    #[serde(flatten)]
1467    pub mode: ElicitationMode,
1468    /// A human-readable message describing what input is needed.
1469    pub message: String,
1470    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1471    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1472    /// these keys.
1473    ///
1474    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1475    #[serde_as(deserialize_as = "DefaultOnError")]
1476    #[schemars(extend("x-deserialize-default-on-error" = true))]
1477    #[serde(default)]
1478    #[serde(rename = "_meta")]
1479    pub meta: Option<Meta>,
1480}
1481
1482impl CreateElicitationRequest {
1483    /// Builds [`CreateElicitationRequest`] with the required request fields set; optional fields start unset or empty.
1484    #[must_use]
1485    pub fn new(mode: impl Into<ElicitationMode>, message: impl Into<String>) -> Self {
1486        Self {
1487            mode: mode.into(),
1488            message: message.into(),
1489            meta: None,
1490        }
1491    }
1492
1493    /// Returns the scope this elicitation is tied to.
1494    #[must_use]
1495    pub fn scope(&self) -> &ElicitationScope {
1496        self.mode.scope()
1497    }
1498
1499    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1500    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1501    /// these keys.
1502    ///
1503    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1504    #[must_use]
1505    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1506        self.meta = meta.into_option();
1507        self
1508    }
1509}
1510
1511/// **UNSTABLE**
1512///
1513/// This capability is not part of the spec yet, and may be removed or changed at any point.
1514///
1515/// The mode of elicitation, determining how user input is collected.
1516#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1517#[serde(tag = "mode", rename_all = "snake_case")]
1518#[non_exhaustive]
1519pub enum ElicitationMode {
1520    /// Form-based elicitation where the client renders a form from the provided schema.
1521    Form(ElicitationFormMode),
1522    /// URL-based elicitation where the client directs the user to a URL.
1523    Url(ElicitationUrlMode),
1524    /// Custom or future elicitation mode.
1525    ///
1526    /// Values beginning with `_` are reserved for implementation-specific
1527    /// extensions. Unknown values that do not begin with `_` are reserved for
1528    /// future ACP variants.
1529    ///
1530    /// Clients that do not understand this mode should preserve the raw payload
1531    /// when storing, replaying, proxying, or forwarding elicitation requests.
1532    /// They MUST NOT render it as a known elicitation mode.
1533    #[serde(untagged)]
1534    Other(OtherElicitationMode),
1535}
1536
1537/// Custom or future elicitation mode payload.
1538///
1539/// This preserves the unknown `mode` discriminator and the rest of the mode
1540/// object for clients that store, replay, proxy, or forward elicitation
1541/// requests.
1542#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq)]
1543#[schemars(inline)]
1544#[schemars(transform = other_elicitation_mode_schema)]
1545#[serde(rename_all = "camelCase")]
1546#[non_exhaustive]
1547pub struct OtherElicitationMode {
1548    /// Custom or future elicitation mode.
1549    ///
1550    /// Values beginning with `_` are reserved for implementation-specific
1551    /// extensions. Unknown values that do not begin with `_` are reserved for
1552    /// future ACP variants.
1553    pub mode: String,
1554    /// The scope this elicitation is tied to.
1555    #[serde(flatten)]
1556    pub scope: ElicitationScope,
1557    /// Additional fields from the unknown elicitation mode payload.
1558    #[serde(flatten)]
1559    pub fields: BTreeMap<String, serde_json::Value>,
1560}
1561
1562impl OtherElicitationMode {
1563    /// Builds [`OtherElicitationMode`] from an unknown discriminator and preserves the remaining extension fields.
1564    #[must_use]
1565    pub fn new(
1566        mode: impl Into<String>,
1567        scope: impl Into<ElicitationScope>,
1568        mut fields: BTreeMap<String, serde_json::Value>,
1569    ) -> Self {
1570        fields.remove("mode");
1571        remove_elicitation_scope_fields(&mut fields);
1572        Self {
1573            mode: mode.into(),
1574            scope: scope.into(),
1575            fields,
1576        }
1577    }
1578}
1579
1580impl<'de> Deserialize<'de> for OtherElicitationMode {
1581    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1582    where
1583        D: serde::Deserializer<'de>,
1584    {
1585        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1586        let mode = fields
1587            .remove("mode")
1588            .ok_or_else(|| serde::de::Error::missing_field("mode"))?;
1589        let serde_json::Value::String(mode) = mode else {
1590            return Err(serde::de::Error::custom("`mode` must be a string"));
1591        };
1592
1593        if is_known_elicitation_mode(&mode) {
1594            return Err(serde::de::Error::custom(format!(
1595                "known elicitation mode `{mode}` did not match its schema"
1596            )));
1597        }
1598
1599        let scope = serde_json::from_value::<ElicitationScope>(serde_json::Value::Object(
1600            fields.clone().into_iter().collect(),
1601        ))
1602        .map_err(serde::de::Error::custom)?;
1603        remove_elicitation_scope_fields(&mut fields);
1604
1605        Ok(Self {
1606            mode,
1607            scope,
1608            fields,
1609        })
1610    }
1611}
1612
1613const KNOWN_ELICITATION_MODES: &[&str] = &["form", "url"];
1614
1615fn is_known_elicitation_mode(mode: &str) -> bool {
1616    KNOWN_ELICITATION_MODES.contains(&mode)
1617}
1618
1619fn remove_elicitation_scope_fields(fields: &mut BTreeMap<String, serde_json::Value>) {
1620    fields.remove("sessionId");
1621    fields.remove("toolCallId");
1622    fields.remove("requestId");
1623}
1624
1625fn other_elicitation_mode_schema(schema: &mut Schema) {
1626    let known_value_schemas: Vec<_> = KNOWN_ELICITATION_MODES
1627        .iter()
1628        .map(|value| {
1629            serde_json::json!({
1630                "properties": {
1631                    "mode": {
1632                        "const": value,
1633                        "type": "string"
1634                    }
1635                },
1636                "required": ["mode"],
1637                "type": "object"
1638            })
1639        })
1640        .collect();
1641
1642    schema.insert(
1643        "not".into(),
1644        serde_json::json!({
1645            "anyOf": known_value_schemas
1646        }),
1647    );
1648}
1649
1650impl From<ElicitationFormMode> for ElicitationMode {
1651    fn from(mode: ElicitationFormMode) -> Self {
1652        Self::Form(mode)
1653    }
1654}
1655
1656impl From<ElicitationUrlMode> for ElicitationMode {
1657    fn from(mode: ElicitationUrlMode) -> Self {
1658        Self::Url(mode)
1659    }
1660}
1661
1662impl From<OtherElicitationMode> for ElicitationMode {
1663    fn from(mode: OtherElicitationMode) -> Self {
1664        Self::Other(mode)
1665    }
1666}
1667
1668impl ElicitationMode {
1669    /// Returns the scope this elicitation mode is tied to.
1670    #[must_use]
1671    pub fn scope(&self) -> &ElicitationScope {
1672        match self {
1673            Self::Form(f) => &f.scope,
1674            Self::Url(u) => &u.scope,
1675            Self::Other(other) => &other.scope,
1676        }
1677    }
1678}
1679
1680/// **UNSTABLE**
1681///
1682/// This capability is not part of the spec yet, and may be removed or changed at any point.
1683///
1684/// Form-based elicitation mode where the client renders a form from the provided schema.
1685#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1686#[serde(rename_all = "camelCase")]
1687#[non_exhaustive]
1688pub struct ElicitationFormMode {
1689    /// The scope this elicitation is tied to.
1690    #[serde(flatten)]
1691    pub scope: ElicitationScope,
1692    /// A JSON Schema describing the form fields to present to the user.
1693    pub requested_schema: ElicitationSchema,
1694}
1695
1696impl ElicitationFormMode {
1697    /// Builds [`ElicitationFormMode`] with the required fields set; optional fields start unset or empty.
1698    #[must_use]
1699    pub fn new(scope: impl Into<ElicitationScope>, requested_schema: ElicitationSchema) -> Self {
1700        Self {
1701            scope: scope.into(),
1702            requested_schema,
1703        }
1704    }
1705}
1706
1707/// **UNSTABLE**
1708///
1709/// This capability is not part of the spec yet, and may be removed or changed at any point.
1710///
1711/// URL-based elicitation mode where the client directs the user to a URL.
1712#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1713#[serde(rename_all = "camelCase")]
1714#[non_exhaustive]
1715pub struct ElicitationUrlMode {
1716    /// The scope this elicitation is tied to.
1717    #[serde(flatten)]
1718    pub scope: ElicitationScope,
1719    /// The unique identifier for this elicitation.
1720    pub elicitation_id: ElicitationId,
1721    /// The URL to direct the user to.
1722    #[schemars(extend("format" = "uri"))]
1723    pub url: String,
1724}
1725
1726impl ElicitationUrlMode {
1727    /// Builds [`ElicitationUrlMode`] with the required fields set; optional fields start unset or empty.
1728    #[must_use]
1729    pub fn new(
1730        scope: impl Into<ElicitationScope>,
1731        elicitation_id: impl Into<ElicitationId>,
1732        url: impl Into<String>,
1733    ) -> Self {
1734        Self {
1735            scope: scope.into(),
1736            elicitation_id: elicitation_id.into(),
1737            url: url.into(),
1738        }
1739    }
1740}
1741
1742/// **UNSTABLE**
1743///
1744/// This capability is not part of the spec yet, and may be removed or changed at any point.
1745///
1746/// Response from the client to an elicitation request.
1747#[serde_as]
1748#[skip_serializing_none]
1749#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1750#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1751#[serde(rename_all = "camelCase")]
1752#[non_exhaustive]
1753pub struct CreateElicitationResponse {
1754    /// The user's action in response to the elicitation.
1755    #[serde(flatten)]
1756    pub action: ElicitationAction,
1757    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1758    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1759    /// these keys.
1760    ///
1761    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1762    #[serde_as(deserialize_as = "DefaultOnError")]
1763    #[schemars(extend("x-deserialize-default-on-error" = true))]
1764    #[serde(default)]
1765    #[serde(rename = "_meta")]
1766    pub meta: Option<Meta>,
1767}
1768
1769impl CreateElicitationResponse {
1770    /// Builds [`CreateElicitationResponse`] with the required response fields set; optional fields start unset or empty.
1771    #[must_use]
1772    pub fn new(action: impl Into<ElicitationAction>) -> Self {
1773        Self {
1774            action: action.into(),
1775            meta: None,
1776        }
1777    }
1778
1779    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1780    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1781    /// these keys.
1782    ///
1783    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
1784    #[must_use]
1785    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1786        self.meta = meta.into_option();
1787        self
1788    }
1789}
1790
1791/// **UNSTABLE**
1792///
1793/// This capability is not part of the spec yet, and may be removed or changed at any point.
1794///
1795/// The user's action in response to an elicitation.
1796#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1797#[serde(tag = "action", rename_all = "snake_case")]
1798#[non_exhaustive]
1799pub enum ElicitationAction {
1800    /// The user accepted and provided content.
1801    Accept(ElicitationAcceptAction),
1802    /// The user declined the elicitation.
1803    Decline,
1804    /// The elicitation was cancelled.
1805    Cancel,
1806    /// Custom or future elicitation action.
1807    ///
1808    /// Values beginning with `_` are reserved for implementation-specific
1809    /// extensions. Unknown values that do not begin with `_` are reserved for
1810    /// future ACP variants.
1811    ///
1812    /// Agents that do not understand this action should preserve the raw
1813    /// payload when storing, replaying, proxying, or forwarding elicitation
1814    /// responses. They MUST NOT treat it as a known elicitation action.
1815    #[serde(untagged)]
1816    Other(OtherElicitationAction),
1817}
1818
1819/// Custom or future elicitation action payload.
1820///
1821/// This preserves the unknown `action` discriminator and the rest of the
1822/// response object for agents that store, replay, proxy, or forward elicitation
1823/// responses.
1824#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq)]
1825#[schemars(inline)]
1826#[schemars(transform = other_elicitation_action_schema)]
1827#[serde(rename_all = "camelCase")]
1828#[non_exhaustive]
1829pub struct OtherElicitationAction {
1830    /// Custom or future elicitation action.
1831    ///
1832    /// Values beginning with `_` are reserved for implementation-specific
1833    /// extensions. Unknown values that do not begin with `_` are reserved for
1834    /// future ACP variants.
1835    pub action: String,
1836    /// Additional fields from the unknown elicitation action payload.
1837    #[serde(flatten)]
1838    pub fields: BTreeMap<String, serde_json::Value>,
1839}
1840
1841impl OtherElicitationAction {
1842    /// Builds [`OtherElicitationAction`] from an unknown discriminator and preserves the remaining extension fields.
1843    #[must_use]
1844    pub fn new(action: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1845        fields.remove("action");
1846        Self {
1847            action: action.into(),
1848            fields,
1849        }
1850    }
1851}
1852
1853impl<'de> Deserialize<'de> for OtherElicitationAction {
1854    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1855    where
1856        D: serde::Deserializer<'de>,
1857    {
1858        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1859        let action = fields
1860            .remove("action")
1861            .ok_or_else(|| serde::de::Error::missing_field("action"))?;
1862        let serde_json::Value::String(action) = action else {
1863            return Err(serde::de::Error::custom("`action` must be a string"));
1864        };
1865
1866        if is_known_elicitation_action(&action) {
1867            return Err(serde::de::Error::custom(format!(
1868                "known elicitation action `{action}` did not match its schema"
1869            )));
1870        }
1871
1872        Ok(Self { action, fields })
1873    }
1874}
1875
1876const KNOWN_ELICITATION_ACTIONS: &[&str] = &["accept", "decline", "cancel"];
1877
1878fn is_known_elicitation_action(action: &str) -> bool {
1879    KNOWN_ELICITATION_ACTIONS.contains(&action)
1880}
1881
1882fn other_elicitation_action_schema(schema: &mut Schema) {
1883    let known_value_schemas: Vec<_> = KNOWN_ELICITATION_ACTIONS
1884        .iter()
1885        .map(|value| {
1886            serde_json::json!({
1887                "properties": {
1888                    "action": {
1889                        "const": value,
1890                        "type": "string"
1891                    }
1892                },
1893                "required": ["action"],
1894                "type": "object"
1895            })
1896        })
1897        .collect();
1898
1899    schema.insert(
1900        "not".into(),
1901        serde_json::json!({
1902            "anyOf": known_value_schemas
1903        }),
1904    );
1905}
1906
1907impl From<ElicitationAcceptAction> for ElicitationAction {
1908    fn from(action: ElicitationAcceptAction) -> Self {
1909        Self::Accept(action)
1910    }
1911}
1912
1913impl From<OtherElicitationAction> for ElicitationAction {
1914    fn from(action: OtherElicitationAction) -> Self {
1915        Self::Other(action)
1916    }
1917}
1918
1919/// **UNSTABLE**
1920///
1921/// This capability is not part of the spec yet, and may be removed or changed at any point.
1922///
1923/// The user accepted the elicitation and provided content.
1924#[serde_as]
1925#[skip_serializing_none]
1926#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1927#[serde(rename_all = "camelCase")]
1928#[non_exhaustive]
1929pub struct ElicitationAcceptAction {
1930    /// The user-provided content, if any, as an object matching the requested schema.
1931    #[serde(default)]
1932    pub content: Option<BTreeMap<String, ElicitationContentValue>>,
1933}
1934
1935impl ElicitationAcceptAction {
1936    /// Builds [`ElicitationAcceptAction`] with the required fields set; optional fields start unset or empty.
1937    #[must_use]
1938    pub fn new() -> Self {
1939        Self { content: None }
1940    }
1941
1942    /// The user-provided content as an object matching the requested schema.
1943    #[must_use]
1944    pub fn content(
1945        mut self,
1946        content: impl IntoOption<BTreeMap<String, ElicitationContentValue>>,
1947    ) -> Self {
1948        self.content = content.into_option();
1949        self
1950    }
1951}
1952
1953/// Allowed wire representations for [`ElicitationContentValue`].
1954#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1955#[serde(untagged)]
1956#[non_exhaustive]
1957pub enum ElicitationContentValue {
1958    /// String value accepted in elicitation response content.
1959    String(String),
1960    /// Integer value accepted in elicitation response content.
1961    Integer(i64),
1962    /// Number value accepted in elicitation response content.
1963    Number(f64),
1964    /// Boolean value accepted in elicitation response content.
1965    Boolean(bool),
1966    /// String array value accepted in elicitation response content.
1967    StringArray(Vec<String>),
1968}
1969
1970impl From<String> for ElicitationContentValue {
1971    fn from(value: String) -> Self {
1972        Self::String(value)
1973    }
1974}
1975
1976impl From<&str> for ElicitationContentValue {
1977    fn from(value: &str) -> Self {
1978        Self::String(value.to_string())
1979    }
1980}
1981
1982impl From<i64> for ElicitationContentValue {
1983    fn from(value: i64) -> Self {
1984        Self::Integer(value)
1985    }
1986}
1987
1988impl From<i32> for ElicitationContentValue {
1989    fn from(value: i32) -> Self {
1990        Self::Integer(i64::from(value))
1991    }
1992}
1993
1994impl From<f64> for ElicitationContentValue {
1995    fn from(value: f64) -> Self {
1996        Self::Number(value)
1997    }
1998}
1999
2000impl From<bool> for ElicitationContentValue {
2001    fn from(value: bool) -> Self {
2002        Self::Boolean(value)
2003    }
2004}
2005
2006impl From<Vec<String>> for ElicitationContentValue {
2007    fn from(value: Vec<String>) -> Self {
2008        Self::StringArray(value)
2009    }
2010}
2011
2012impl From<Vec<&str>> for ElicitationContentValue {
2013    fn from(value: Vec<&str>) -> Self {
2014        Self::StringArray(value.into_iter().map(str::to_string).collect())
2015    }
2016}
2017
2018impl Default for ElicitationAcceptAction {
2019    fn default() -> Self {
2020        Self::new()
2021    }
2022}
2023
2024/// **UNSTABLE**
2025///
2026/// This capability is not part of the spec yet, and may be removed or changed at any point.
2027///
2028/// Notification sent by the agent when a URL-based elicitation is complete.
2029#[serde_as]
2030#[skip_serializing_none]
2031#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
2032#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))]
2033#[serde(rename_all = "camelCase")]
2034#[non_exhaustive]
2035pub struct CompleteElicitationNotification {
2036    /// The ID of the elicitation that completed.
2037    pub elicitation_id: ElicitationId,
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    #[schemars(extend("x-deserialize-default-on-error" = true))]
2045    #[serde(default)]
2046    #[serde(rename = "_meta")]
2047    pub meta: Option<Meta>,
2048}
2049
2050impl CompleteElicitationNotification {
2051    /// Builds [`CompleteElicitationNotification`] with the required notification fields set; optional fields start unset or empty.
2052    #[must_use]
2053    pub fn new(elicitation_id: impl Into<ElicitationId>) -> Self {
2054        Self {
2055            elicitation_id: elicitation_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#[cfg(test)]
2073mod tests {
2074    use super::*;
2075    use serde_json::json;
2076
2077    #[test]
2078    fn form_mode_request_serialization() {
2079        let schema = ElicitationSchema::new().string("name", true);
2080        let req = CreateElicitationRequest::new(
2081            ElicitationFormMode::new(ElicitationSessionScope::new("sess_1"), schema),
2082            "Please enter your name",
2083        );
2084
2085        let json = serde_json::to_value(&req).unwrap();
2086        assert_eq!(json["sessionId"], "sess_1");
2087        assert!(json.get("toolCallId").is_none());
2088        assert_eq!(json["mode"], "form");
2089        assert_eq!(json["message"], "Please enter your name");
2090        assert!(json["requestedSchema"].is_object());
2091        assert_eq!(json["requestedSchema"]["type"], "object");
2092        assert_eq!(
2093            json["requestedSchema"]["properties"]["name"]["type"],
2094            "string"
2095        );
2096
2097        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2098        assert_eq!(
2099            *roundtripped.scope(),
2100            ElicitationSessionScope::new("sess_1").into()
2101        );
2102        assert_eq!(roundtripped.message, "Please enter your name");
2103        assert!(matches!(roundtripped.mode, ElicitationMode::Form(_)));
2104    }
2105
2106    #[test]
2107    fn url_mode_request_serialization() {
2108        let req = CreateElicitationRequest::new(
2109            ElicitationUrlMode::new(
2110                ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
2111                "elic_1",
2112                "https://example.com/auth",
2113            ),
2114            "Please authenticate",
2115        );
2116
2117        let json = serde_json::to_value(&req).unwrap();
2118        assert_eq!(json["sessionId"], "sess_2");
2119        assert_eq!(json["toolCallId"], "tc_1");
2120        assert_eq!(json["mode"], "url");
2121        assert_eq!(json["elicitationId"], "elic_1");
2122        assert_eq!(json["url"], "https://example.com/auth");
2123        assert_eq!(json["message"], "Please authenticate");
2124
2125        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2126        assert_eq!(
2127            *roundtripped.scope(),
2128            ElicitationSessionScope::new("sess_2")
2129                .tool_call_id("tc_1")
2130                .into()
2131        );
2132        assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
2133    }
2134
2135    #[test]
2136    fn response_accept_serialization() {
2137        let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
2138            ElicitationAcceptAction::new().content(BTreeMap::from([(
2139                "name".to_string(),
2140                ElicitationContentValue::from("Alice"),
2141            )])),
2142        ));
2143
2144        let json = serde_json::to_value(&resp).unwrap();
2145        assert_eq!(json["action"], "accept");
2146        assert_eq!(json["content"]["name"], "Alice");
2147
2148        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2149        assert!(matches!(
2150            roundtripped.action,
2151            ElicitationAction::Accept(ElicitationAcceptAction {
2152                content: Some(_),
2153                ..
2154            })
2155        ));
2156    }
2157
2158    #[test]
2159    fn response_decline_serialization() {
2160        let resp = CreateElicitationResponse::new(ElicitationAction::Decline);
2161
2162        let json = serde_json::to_value(&resp).unwrap();
2163        assert_eq!(json["action"], "decline");
2164
2165        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2166        assert!(matches!(roundtripped.action, ElicitationAction::Decline));
2167    }
2168
2169    #[test]
2170    fn response_cancel_serialization() {
2171        let resp = CreateElicitationResponse::new(ElicitationAction::Cancel);
2172
2173        let json = serde_json::to_value(&resp).unwrap();
2174        assert_eq!(json["action"], "cancel");
2175
2176        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2177        assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
2178    }
2179
2180    #[test]
2181    fn unknown_action_response_serialization() {
2182        let json = json!({
2183            "action": "_defer",
2184            "reason": "waiting",
2185            "retryAfterMs": 1000
2186        });
2187
2188        let resp: CreateElicitationResponse = serde_json::from_value(json.clone()).unwrap();
2189        let ElicitationAction::Other(other) = &resp.action else {
2190            panic!("expected unknown elicitation action");
2191        };
2192
2193        assert_eq!(other.action, "_defer");
2194        assert_eq!(other.fields.get("reason"), Some(&json!("waiting")));
2195        assert_eq!(other.fields.get("retryAfterMs"), Some(&json!(1000)));
2196        assert_eq!(serde_json::to_value(&resp).unwrap(), json);
2197    }
2198
2199    #[test]
2200    fn unknown_action_does_not_hide_known_action() {
2201        assert!(
2202            serde_json::from_value::<OtherElicitationAction>(json!({
2203                "action": "accept",
2204                "content": {}
2205            }))
2206            .is_err()
2207        );
2208        assert!(serde_json::from_value::<ElicitationAction>(json!({})).is_err());
2209    }
2210
2211    #[test]
2212    fn url_mode_request_scope_serialization() {
2213        let req = CreateElicitationRequest::new(
2214            ElicitationUrlMode::new(
2215                ElicitationRequestScope::new(RequestId::Number(42)),
2216                "elic_2",
2217                "https://example.com/setup",
2218            ),
2219            "Please complete setup",
2220        );
2221
2222        let json = serde_json::to_value(&req).unwrap();
2223        assert_eq!(json["requestId"], 42);
2224        assert!(json.get("sessionId").is_none());
2225        assert_eq!(json["mode"], "url");
2226        assert_eq!(json["elicitationId"], "elic_2");
2227        assert_eq!(json["url"], "https://example.com/setup");
2228        assert_eq!(json["message"], "Please complete setup");
2229
2230        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2231        assert_eq!(
2232            *roundtripped.scope(),
2233            ElicitationRequestScope::new(RequestId::Number(42)).into()
2234        );
2235        assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
2236    }
2237
2238    #[test]
2239    fn unknown_mode_request_serialization() {
2240        let json = json!({
2241            "requestId": 42,
2242            "mode": "_browser",
2243            "message": "Open a browser window",
2244            "target": "login"
2245        });
2246
2247        let req: CreateElicitationRequest = serde_json::from_value(json.clone()).unwrap();
2248        let ElicitationMode::Other(other) = &req.mode else {
2249            panic!("expected unknown elicitation mode");
2250        };
2251
2252        assert_eq!(other.mode, "_browser");
2253        assert_eq!(
2254            other.scope,
2255            ElicitationRequestScope::new(RequestId::Number(42)).into()
2256        );
2257        assert_eq!(other.fields.get("target"), Some(&json!("login")));
2258        assert_eq!(
2259            *req.scope(),
2260            ElicitationRequestScope::new(RequestId::Number(42)).into()
2261        );
2262        assert_eq!(serde_json::to_value(&req).unwrap(), json);
2263    }
2264
2265    #[test]
2266    fn unknown_mode_does_not_hide_malformed_known_mode() {
2267        let missing_requested_schema = json!({
2268            "requestId": 42,
2269            "mode": "form",
2270            "message": "Enter your name"
2271        });
2272
2273        assert!(
2274            serde_json::from_value::<CreateElicitationRequest>(missing_requested_schema).is_err()
2275        );
2276        assert!(serde_json::from_value::<ElicitationMode>(json!({})).is_err());
2277    }
2278
2279    #[test]
2280    fn request_scope_request_serialization() {
2281        let req = CreateElicitationRequest::new(
2282            ElicitationFormMode::new(
2283                ElicitationRequestScope::new(RequestId::Number(99)),
2284                ElicitationSchema::new().string("workspace", true),
2285            ),
2286            "Enter workspace name",
2287        );
2288
2289        let json = serde_json::to_value(&req).unwrap();
2290        assert_eq!(json["requestId"], 99);
2291        assert!(json.get("sessionId").is_none());
2292
2293        let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2294        assert_eq!(
2295            *roundtripped.scope(),
2296            ElicitationRequestScope::new(RequestId::Number(99)).into()
2297        );
2298    }
2299
2300    /// `ClientResponse` is `#[serde(untagged)]` with `WriteTextFileResponse` (which has
2301    /// `#[serde(default)]`) listed first, so standalone deserialization is ambiguous.
2302    /// In practice, the RPC layer selects the correct variant based on the originating
2303    /// request method. These tests verify that serialization through `ClientResponse`
2304    /// produces the correct flattened wire format and round-trips back via the
2305    /// concrete `CreateElicitationResponse` type.
2306    #[test]
2307    fn client_response_serialization_accept() {
2308        use crate::v1::ClientResponse;
2309
2310        let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
2311            ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from([(
2312                "name".to_string(),
2313                ElicitationContentValue::from("Alice"),
2314            )]))),
2315        ));
2316        let json = serde_json::to_value(&resp).unwrap();
2317        assert_eq!(json["action"], "accept");
2318        assert_eq!(json["content"]["name"], "Alice");
2319
2320        // Round-trip back through the concrete type
2321        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2322        assert!(matches!(roundtripped.action, ElicitationAction::Accept(_)));
2323    }
2324
2325    #[test]
2326    fn client_response_serialization_decline() {
2327        use crate::v1::ClientResponse;
2328
2329        let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
2330            ElicitationAction::Decline,
2331        ));
2332        let json = serde_json::to_value(&resp).unwrap();
2333        assert_eq!(json["action"], "decline");
2334
2335        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2336        assert!(matches!(roundtripped.action, ElicitationAction::Decline));
2337    }
2338
2339    #[test]
2340    fn client_response_serialization_cancel() {
2341        use crate::v1::ClientResponse;
2342
2343        let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
2344            ElicitationAction::Cancel,
2345        ));
2346        let json = serde_json::to_value(&resp).unwrap();
2347        assert_eq!(json["action"], "cancel");
2348
2349        let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
2350        assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
2351    }
2352
2353    /// Guard against serde regressions with the `flatten` + internally-tagged combination.
2354    /// Extra fields in the JSON must not cause deserialization failures.
2355    #[test]
2356    fn request_tolerates_extra_fields() {
2357        let json = json!({
2358            "sessionId": "sess_1",
2359            "mode": "form",
2360            "message": "Enter your name",
2361            "requestedSchema": {
2362                "type": "object",
2363                "properties": {
2364                    "name": { "type": "string", "title": "Name" }
2365                },
2366                "required": ["name"]
2367            },
2368            "unknownStringField": "hello",
2369            "unknownNumberField": 42
2370        });
2371
2372        let req: CreateElicitationRequest = serde_json::from_value(json).unwrap();
2373        assert_eq!(*req.scope(), ElicitationSessionScope::new("sess_1").into());
2374        assert_eq!(req.message, "Enter your name");
2375        assert!(matches!(req.mode, ElicitationMode::Form(_)));
2376    }
2377
2378    #[test]
2379    fn completion_notification_serialization() {
2380        let notif = CompleteElicitationNotification::new("elic_1");
2381
2382        let json = serde_json::to_value(&notif).unwrap();
2383        assert_eq!(json["elicitationId"], "elic_1");
2384
2385        let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap();
2386        assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1"));
2387    }
2388
2389    #[test]
2390    fn capabilities_form_only() {
2391        let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new());
2392
2393        let json = serde_json::to_value(&caps).unwrap();
2394        assert!(json["form"].is_object());
2395        assert!(json.get("url").is_none());
2396
2397        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
2398        assert!(roundtripped.form.is_some());
2399        assert!(roundtripped.url.is_none());
2400    }
2401
2402    #[test]
2403    fn capabilities_url_only() {
2404        let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new());
2405
2406        let json = serde_json::to_value(&caps).unwrap();
2407        assert!(json.get("form").is_none());
2408        assert!(json["url"].is_object());
2409
2410        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
2411        assert!(roundtripped.form.is_none());
2412        assert!(roundtripped.url.is_some());
2413    }
2414
2415    #[test]
2416    fn capabilities_both() {
2417        let caps = ElicitationCapabilities::new()
2418            .form(ElicitationFormCapabilities::new())
2419            .url(ElicitationUrlCapabilities::new());
2420
2421        let json = serde_json::to_value(&caps).unwrap();
2422        assert!(json["form"].is_object());
2423        assert!(json["url"].is_object());
2424
2425        let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
2426        assert!(roundtripped.form.is_some());
2427        assert!(roundtripped.url.is_some());
2428    }
2429
2430    #[test]
2431    fn schema_default_sets_object_type() {
2432        let schema = ElicitationSchema::default();
2433
2434        assert_eq!(schema.type_, ElicitationSchemaType::Object);
2435        assert!(schema.properties.is_empty());
2436
2437        let json = serde_json::to_value(&schema).unwrap();
2438        assert_eq!(json["type"], "object");
2439    }
2440
2441    #[test]
2442    fn schema_builder_serialization() {
2443        let schema = ElicitationSchema::new()
2444            .string("name", true)
2445            .email("email", true)
2446            .integer("age", 0, 150, true)
2447            .boolean("newsletter", false)
2448            .description("User registration");
2449
2450        let json = serde_json::to_value(&schema).unwrap();
2451        assert_eq!(json["type"], "object");
2452        assert_eq!(json["description"], "User registration");
2453        assert_eq!(json["properties"]["name"]["type"], "string");
2454        assert_eq!(json["properties"]["email"]["type"], "string");
2455        assert_eq!(json["properties"]["email"]["format"], "email");
2456        assert_eq!(json["properties"]["age"]["type"], "integer");
2457        assert_eq!(json["properties"]["age"]["minimum"], 0);
2458        assert_eq!(json["properties"]["age"]["maximum"], 150);
2459        assert_eq!(json["properties"]["newsletter"]["type"], "boolean");
2460
2461        let required = json["required"].as_array().unwrap();
2462        assert!(required.contains(&json!("name")));
2463        assert!(required.contains(&json!("email")));
2464        assert!(required.contains(&json!("age")));
2465        assert!(!required.contains(&json!("newsletter")));
2466
2467        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2468        assert_eq!(roundtripped.properties.len(), 4);
2469        assert!(roundtripped.required.unwrap().contains(&"name".to_string()));
2470    }
2471
2472    #[test]
2473    fn schema_string_enum_serialization() {
2474        let schema = ElicitationSchema::new().property(
2475            "color",
2476            StringPropertySchema::new().enum_values(vec![
2477                "red".into(),
2478                "green".into(),
2479                "blue".into(),
2480            ]),
2481            true,
2482        );
2483
2484        let json = serde_json::to_value(&schema).unwrap();
2485        assert_eq!(json["properties"]["color"]["type"], "string");
2486        let enum_vals = json["properties"]["color"]["enum"].as_array().unwrap();
2487        assert_eq!(enum_vals.len(), 3);
2488
2489        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2490        if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("color").unwrap()
2491        {
2492            assert_eq!(s.enum_values.as_ref().unwrap().len(), 3);
2493        } else {
2494            panic!("expected String variant");
2495        }
2496    }
2497
2498    #[test]
2499    fn schema_multi_select_serialization() {
2500        let schema = ElicitationSchema::new().property(
2501            "colors",
2502            MultiSelectPropertySchema::new(vec!["red".into(), "green".into(), "blue".into()])
2503                .min_items(1)
2504                .max_items(3),
2505            false,
2506        );
2507
2508        let json = serde_json::to_value(&schema).unwrap();
2509        assert_eq!(json["properties"]["colors"]["type"], "array");
2510        assert_eq!(json["properties"]["colors"]["items"]["type"], "string");
2511        assert_eq!(json["properties"]["colors"]["minItems"], 1);
2512        assert_eq!(json["properties"]["colors"]["maxItems"], 3);
2513
2514        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2515        let ElicitationPropertySchema::Array(array) =
2516            roundtripped.properties.get("colors").unwrap()
2517        else {
2518            panic!("expected Array variant");
2519        };
2520        let MultiSelectItems::String(items) = &array.items else {
2521            panic!("expected String multi-select items");
2522        };
2523        assert_eq!(items.values.len(), 3);
2524    }
2525
2526    #[test]
2527    fn multi_select_titled_items_keep_mcp_shape() {
2528        let items = MultiSelectItems::Titled(TitledMultiSelectItems::new(vec![EnumOption::new(
2529            "#ff0000", "Red",
2530        )]));
2531
2532        let json = serde_json::to_value(&items).unwrap();
2533        assert!(json.get("type").is_none());
2534        assert_eq!(json["anyOf"][0]["const"], "#ff0000");
2535        assert_eq!(json["anyOf"][0]["title"], "Red");
2536
2537        let roundtripped: MultiSelectItems = serde_json::from_value(json).unwrap();
2538        assert!(matches!(roundtripped, MultiSelectItems::Titled(_)));
2539    }
2540
2541    #[test]
2542    fn multi_select_items_preserve_unknown_type() {
2543        let json = json!({
2544            "type": "_token",
2545            "format": "workspace",
2546            "anyOf": [
2547                { "const": "repo", "title": "Repository" }
2548            ]
2549        });
2550
2551        let items: MultiSelectItems = serde_json::from_value(json.clone()).unwrap();
2552        let MultiSelectItems::Other(other) = &items else {
2553            panic!("expected unknown multi-select items");
2554        };
2555
2556        assert_eq!(other.type_, "_token");
2557        assert_eq!(other.fields.get("format"), Some(&json!("workspace")));
2558        assert_eq!(other.fields.get("anyOf"), Some(&json["anyOf"]));
2559        assert_eq!(serde_json::to_value(&items).unwrap(), json);
2560    }
2561
2562    #[test]
2563    fn multi_select_items_unknown_does_not_hide_malformed_string_type() {
2564        assert!(
2565            serde_json::from_value::<MultiSelectItems>(json!({
2566                "type": "string"
2567            }))
2568            .is_err()
2569        );
2570        assert!(
2571            serde_json::from_value::<OtherMultiSelectItems>(json!({
2572                "type": "string",
2573                "format": "workspace"
2574            }))
2575            .is_err()
2576        );
2577    }
2578
2579    #[test]
2580    fn property_schema_preserves_unknown_type() {
2581        let schema: ElicitationSchema = serde_json::from_value(json!({
2582            "type": "object",
2583            "properties": {
2584                "location": {
2585                    "type": "_location",
2586                    "title": "Location",
2587                    "precision": "city"
2588                }
2589            }
2590        }))
2591        .unwrap();
2592
2593        let ElicitationPropertySchema::Other(unknown) = schema.properties.get("location").unwrap()
2594        else {
2595            panic!("expected unknown property schema");
2596        };
2597
2598        assert_eq!(unknown.type_, "_location");
2599        assert_eq!(unknown.fields.get("title"), Some(&json!("Location")));
2600        assert_eq!(unknown.fields.get("precision"), Some(&json!("city")));
2601        assert_eq!(
2602            serde_json::to_value(ElicitationPropertySchema::Other(unknown.clone())).unwrap(),
2603            json!({
2604                "type": "_location",
2605                "title": "Location",
2606                "precision": "city"
2607            })
2608        );
2609    }
2610
2611    #[test]
2612    fn property_schema_unknown_does_not_hide_malformed_known_type() {
2613        assert!(
2614            serde_json::from_value::<ElicitationPropertySchema>(json!({
2615                "type": "array"
2616            }))
2617            .is_err()
2618        );
2619        assert!(serde_json::from_value::<ElicitationPropertySchema>(json!({})).is_err());
2620    }
2621
2622    #[test]
2623    fn schema_titled_enum_serialization() {
2624        let schema = ElicitationSchema::new().property(
2625            "country",
2626            StringPropertySchema::new().one_of(vec![
2627                EnumOption::new("us", "United States").description("Use US English spelling."),
2628                EnumOption::new("uk", "United Kingdom"),
2629            ]),
2630            true,
2631        );
2632
2633        let json = serde_json::to_value(&schema).unwrap();
2634        assert_eq!(json["properties"]["country"]["type"], "string");
2635        let one_of = json["properties"]["country"]["oneOf"].as_array().unwrap();
2636        assert_eq!(one_of.len(), 2);
2637        assert_eq!(one_of[0]["const"], "us");
2638        assert_eq!(one_of[0]["title"], "United States");
2639        assert_eq!(one_of[0]["description"], "Use US English spelling.");
2640        assert!(one_of[1].get("description").is_none());
2641
2642        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2643        if let ElicitationPropertySchema::String(s) =
2644            roundtripped.properties.get("country").unwrap()
2645        {
2646            let one_of = s.one_of.as_ref().unwrap();
2647            assert_eq!(one_of.len(), 2);
2648            assert_eq!(
2649                one_of[0].description.as_deref(),
2650                Some("Use US English spelling.")
2651            );
2652            assert!(one_of[1].description.is_none());
2653        } else {
2654            panic!("expected String variant");
2655        }
2656    }
2657
2658    #[test]
2659    fn schema_number_property_serialization() {
2660        let schema = ElicitationSchema::new().number("rating", 0.0, 5.0, true);
2661
2662        let json = serde_json::to_value(&schema).unwrap();
2663        assert_eq!(json["properties"]["rating"]["type"], "number");
2664        assert_eq!(json["properties"]["rating"]["minimum"], 0.0);
2665        assert_eq!(json["properties"]["rating"]["maximum"], 5.0);
2666
2667        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2668        if let ElicitationPropertySchema::Number(n) = roundtripped.properties.get("rating").unwrap()
2669        {
2670            assert_eq!(n.minimum, Some(0.0));
2671            assert_eq!(n.maximum, Some(5.0));
2672        } else {
2673            panic!("expected Number variant");
2674        }
2675    }
2676
2677    #[test]
2678    fn schema_string_format_serialization() {
2679        let schema = ElicitationSchema::new()
2680            .uri("website", true)
2681            .date("birthday", true)
2682            .date_time("updated_at", false);
2683
2684        let json = serde_json::to_value(&schema).unwrap();
2685        assert_eq!(json["properties"]["website"]["type"], "string");
2686        assert_eq!(json["properties"]["website"]["format"], "uri");
2687        assert_eq!(json["properties"]["birthday"]["type"], "string");
2688        assert_eq!(json["properties"]["birthday"]["format"], "date");
2689        assert_eq!(json["properties"]["updated_at"]["type"], "string");
2690        assert_eq!(json["properties"]["updated_at"]["format"], "date-time");
2691
2692        let required = json["required"].as_array().unwrap();
2693        assert!(required.contains(&json!("website")));
2694        assert!(required.contains(&json!("birthday")));
2695        assert!(!required.contains(&json!("updated_at")));
2696    }
2697
2698    #[test]
2699    fn schema_string_pattern_serialization() {
2700        let schema = ElicitationSchema::new().property(
2701            "name",
2702            StringPropertySchema::new()
2703                .min_length(1)
2704                .max_length(64)
2705                .pattern("^[a-zA-Z_][a-zA-Z0-9_]*$"),
2706            true,
2707        );
2708
2709        let json = serde_json::to_value(&schema).unwrap();
2710        assert_eq!(json["properties"]["name"]["type"], "string");
2711        assert_eq!(
2712            json["properties"]["name"]["pattern"],
2713            "^[a-zA-Z_][a-zA-Z0-9_]*$"
2714        );
2715
2716        let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2717        if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("name").unwrap() {
2718            assert_eq!(s.pattern.as_deref(), Some("^[a-zA-Z_][a-zA-Z0-9_]*$"));
2719        } else {
2720            panic!("expected String variant");
2721        }
2722    }
2723
2724    #[test]
2725    fn schema_property_updates_required_state() {
2726        let schema = ElicitationSchema::new()
2727            .string("name", true)
2728            .email("name", false);
2729
2730        let json = serde_json::to_value(&schema).unwrap();
2731        assert!(json.get("required").is_none());
2732        assert_eq!(json["properties"]["name"]["format"], "email");
2733    }
2734
2735    #[test]
2736    fn schema_defaults_invalid_object_type() {
2737        let schema = serde_json::from_value::<ElicitationSchema>(json!({
2738            "type": "array",
2739            "properties": {
2740                "name": {
2741                    "type": "string"
2742                }
2743            }
2744        }))
2745        .unwrap();
2746
2747        assert_eq!(schema.type_, ElicitationSchemaType::Object);
2748        assert!(schema.properties.contains_key("name"));
2749    }
2750
2751    #[test]
2752    fn titled_multi_select_items_reject_one_of() {
2753        let err = serde_json::from_value::<TitledMultiSelectItems>(json!({
2754            "oneOf": [
2755                {
2756                    "const": "red",
2757                    "title": "Red"
2758                }
2759            ]
2760        }))
2761        .unwrap_err();
2762
2763        assert!(err.to_string().contains("missing field `anyOf`"));
2764    }
2765
2766    #[test]
2767    fn response_accept_rejects_non_object_content() {
2768        assert!(
2769            serde_json::from_value::<CreateElicitationResponse>(json!({
2770                "action": "accept",
2771                "content": "Alice"
2772            }))
2773            .is_err()
2774        );
2775    }
2776
2777    #[test]
2778    fn response_accept_rejects_nested_object_content() {
2779        assert!(
2780            serde_json::from_value::<CreateElicitationResponse>(json!({
2781                "action": "accept",
2782                "content": {
2783                    "profile": {
2784                        "name": "Alice"
2785                    }
2786                }
2787            }))
2788            .is_err()
2789        );
2790    }
2791
2792    #[test]
2793    fn response_accept_allows_primitive_and_string_array_content() {
2794        let response = CreateElicitationResponse::new(ElicitationAction::Accept(
2795            ElicitationAcceptAction::new().content(BTreeMap::from([
2796                ("name".to_string(), ElicitationContentValue::from("Alice")),
2797                ("age".to_string(), ElicitationContentValue::from(30_i32)),
2798                ("score".to_string(), ElicitationContentValue::from(9.5_f64)),
2799                (
2800                    "subscribed".to_string(),
2801                    ElicitationContentValue::from(true),
2802                ),
2803                (
2804                    "tags".to_string(),
2805                    ElicitationContentValue::from(vec!["rust", "acp"]),
2806                ),
2807            ])),
2808        ));
2809
2810        let json = serde_json::to_value(&response).unwrap();
2811        assert_eq!(json["action"], "accept");
2812        assert_eq!(json["content"]["name"], "Alice");
2813        assert_eq!(json["content"]["age"], 30);
2814        assert_eq!(json["content"]["score"], 9.5);
2815        assert_eq!(json["content"]["subscribed"], true);
2816        assert_eq!(json["content"]["tags"][0], "rust");
2817        assert_eq!(json["content"]["tags"][1], "acp");
2818    }
2819}