Skip to main content

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