Skip to main content

agent_client_protocol_schema/v1/
content.rs

1//! Content blocks for representing various types of information in the Agent Client Protocol.
2//!
3//! This module defines the core content types used throughout the protocol for communication
4//! between agents and clients. Content blocks provide a flexible, extensible way to represent
5//! text, images, audio, and other resources in prompts, responses, and tool call results.
6//!
7//! The content block structure is designed to be compatible with the Model Context Protocol (MCP),
8//! allowing seamless integration between ACP and MCP-based tools.
9//!
10//! See: [Content](https://agentclientprotocol.com/protocol/content)
11
12use serde::{Deserialize, Serialize};
13use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
14
15use crate::IntoOption;
16
17use super::Meta;
18
19/// Content blocks represent displayable information in the Agent Client Protocol.
20///
21/// They provide a structured way to handle various types of user-facing content—whether
22/// it's text from language models, images for analysis, or embedded resources for context.
23///
24/// Content blocks appear in:
25/// - User prompts sent via `session/prompt`
26/// - Language model output streamed through `session/update` notifications
27/// - Progress updates and results from tool calls
28///
29/// This structure is compatible with the Model Context Protocol (MCP), enabling
30/// agents to seamlessly forward content from MCP tool outputs without transformation.
31///
32/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
34#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
35#[serde(tag = "type", rename_all = "snake_case")]
36#[cfg_attr(feature = "schemars", schemars(extend("discriminator" = {"propertyName": "type"})))]
37#[non_exhaustive]
38pub enum ContentBlock {
39    /// Text content. May be plain text or formatted with Markdown.
40    ///
41    /// All agents MUST support text content blocks in prompts.
42    /// Clients SHOULD render this text as Markdown.
43    Text(TextContent),
44    /// Images for visual context or analysis.
45    ///
46    /// Requires the `image` prompt capability when included in prompts.
47    Image(ImageContent),
48    /// Audio data for transcription or analysis.
49    ///
50    /// Requires the `audio` prompt capability when included in prompts.
51    Audio(AudioContent),
52    /// References to resources that the agent can access.
53    ///
54    /// All agents MUST support resource links in prompts.
55    ResourceLink(ResourceLink),
56    /// Complete resource contents embedded directly in the message.
57    ///
58    /// Preferred for including context as it avoids extra round-trips.
59    ///
60    /// Requires the `embeddedContext` prompt capability when included in prompts.
61    Resource(EmbeddedResource),
62}
63
64/// Text provided to or from an LLM.
65#[serde_as]
66#[skip_serializing_none]
67#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
68#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
69#[non_exhaustive]
70pub struct TextContent {
71    /// Optional annotations that help clients decide how to display or route this content.
72    #[serde_as(deserialize_as = "DefaultOnError")]
73    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
74    #[serde(default)]
75    pub annotations: Option<Annotations>,
76    /// Text payload carried by this content block.
77    pub text: String,
78    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
79    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
80    /// these keys.
81    ///
82    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
83    #[serde_as(deserialize_as = "DefaultOnError")]
84    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
85    #[serde(default)]
86    #[serde(rename = "_meta")]
87    pub meta: Option<Meta>,
88}
89
90impl TextContent {
91    /// Builds [`TextContent`] with its required content payload; optional annotations and metadata start unset.
92    #[must_use]
93    pub fn new(text: impl Into<String>) -> Self {
94        Self {
95            annotations: None,
96            text: text.into(),
97            meta: None,
98        }
99    }
100
101    /// Sets or clears the optional `annotations` field.
102    #[must_use]
103    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
104        self.annotations = annotations.into_option();
105        self
106    }
107
108    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
109    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
110    /// these keys.
111    ///
112    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
113    #[must_use]
114    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
115        self.meta = meta.into_option();
116        self
117    }
118}
119
120impl<T: Into<String>> From<T> for ContentBlock {
121    fn from(value: T) -> Self {
122        Self::Text(TextContent::new(value))
123    }
124}
125
126/// An image provided to or from an LLM.
127#[serde_as]
128#[skip_serializing_none]
129#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
130#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
131#[serde(rename_all = "camelCase")]
132#[non_exhaustive]
133pub struct ImageContent {
134    /// Optional annotations that help clients decide how to display or route this content.
135    #[serde_as(deserialize_as = "DefaultOnError")]
136    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
137    #[serde(default)]
138    pub annotations: Option<Annotations>,
139    /// Base64-encoded media payload.
140    pub data: String,
141    /// MIME type describing the encoded media payload.
142    pub mime_type: String,
143    /// URI associated with this resource or media payload.
144    #[serde_as(deserialize_as = "DefaultOnError")]
145    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
146    #[serde(default)]
147    pub uri: Option<String>,
148    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
149    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
150    /// these keys.
151    ///
152    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
153    #[serde_as(deserialize_as = "DefaultOnError")]
154    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
155    #[serde(default)]
156    #[serde(rename = "_meta")]
157    pub meta: Option<Meta>,
158}
159
160impl ImageContent {
161    /// Builds [`ImageContent`] with its required content payload; optional annotations and metadata start unset.
162    #[must_use]
163    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
164        Self {
165            annotations: None,
166            data: data.into(),
167            mime_type: mime_type.into(),
168            uri: None,
169            meta: None,
170        }
171    }
172
173    /// Sets or clears the optional `annotations` field.
174    #[must_use]
175    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
176        self.annotations = annotations.into_option();
177        self
178    }
179
180    /// Sets or clears the optional `uri` field.
181    #[must_use]
182    pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
183        self.uri = uri.into_option();
184        self
185    }
186
187    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
188    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
189    /// these keys.
190    ///
191    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
192    #[must_use]
193    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
194        self.meta = meta.into_option();
195        self
196    }
197}
198
199/// Audio provided to or from an LLM.
200#[serde_as]
201#[skip_serializing_none]
202#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
203#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
204#[serde(rename_all = "camelCase")]
205#[non_exhaustive]
206pub struct AudioContent {
207    /// Optional annotations that help clients decide how to display or route this content.
208    #[serde_as(deserialize_as = "DefaultOnError")]
209    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
210    #[serde(default)]
211    pub annotations: Option<Annotations>,
212    /// Base64-encoded media payload.
213    pub data: String,
214    /// MIME type describing the encoded media payload.
215    pub mime_type: String,
216    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
217    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
218    /// these keys.
219    ///
220    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
221    #[serde_as(deserialize_as = "DefaultOnError")]
222    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
223    #[serde(default)]
224    #[serde(rename = "_meta")]
225    pub meta: Option<Meta>,
226}
227
228impl AudioContent {
229    /// Builds [`AudioContent`] with its required content payload; optional annotations and metadata start unset.
230    #[must_use]
231    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
232        Self {
233            annotations: None,
234            data: data.into(),
235            mime_type: mime_type.into(),
236            meta: None,
237        }
238    }
239
240    /// Sets or clears the optional `annotations` field.
241    #[must_use]
242    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
243        self.annotations = annotations.into_option();
244        self
245    }
246
247    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
248    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
249    /// these keys.
250    ///
251    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
252    #[must_use]
253    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
254        self.meta = meta.into_option();
255        self
256    }
257}
258
259/// The contents of a resource, embedded into a prompt or tool call result.
260#[serde_as]
261#[skip_serializing_none]
262#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
263#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
264#[non_exhaustive]
265pub struct EmbeddedResource {
266    /// Optional annotations that help clients decide how to display or route this content.
267    #[serde_as(deserialize_as = "DefaultOnError")]
268    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
269    #[serde(default)]
270    pub annotations: Option<Annotations>,
271    /// Embedded resource payload, either text or binary data.
272    pub resource: EmbeddedResourceResource,
273    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
274    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
275    /// these keys.
276    ///
277    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
278    #[serde_as(deserialize_as = "DefaultOnError")]
279    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
280    #[serde(default)]
281    #[serde(rename = "_meta")]
282    pub meta: Option<Meta>,
283}
284
285impl EmbeddedResource {
286    /// Builds [`EmbeddedResource`] with its required content payload; optional annotations and metadata start unset.
287    #[must_use]
288    pub fn new(resource: EmbeddedResourceResource) -> Self {
289        Self {
290            annotations: None,
291            resource,
292            meta: None,
293        }
294    }
295
296    /// Sets or clears the optional `annotations` field.
297    #[must_use]
298    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
299        self.annotations = annotations.into_option();
300        self
301    }
302
303    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
304    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
305    /// these keys.
306    ///
307    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
308    #[must_use]
309    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
310        self.meta = meta.into_option();
311        self
312    }
313}
314
315/// Resource content that can be embedded in a message.
316#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
317#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
318#[serde(untagged)]
319#[non_exhaustive]
320pub enum EmbeddedResourceResource {
321    /// Text resource contents embedded directly in the message.
322    TextResourceContents(TextResourceContents),
323    /// Binary resource contents embedded directly in the message.
324    BlobResourceContents(BlobResourceContents),
325}
326
327/// Text-based resource contents.
328#[serde_as]
329#[skip_serializing_none]
330#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
331#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
332#[serde(rename_all = "camelCase")]
333#[non_exhaustive]
334pub struct TextResourceContents {
335    /// MIME type describing the encoded media payload.
336    #[serde_as(deserialize_as = "DefaultOnError")]
337    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
338    #[serde(default)]
339    pub mime_type: Option<String>,
340    /// Text payload carried by this content block.
341    pub text: String,
342    /// URI associated with this resource or media payload.
343    pub uri: String,
344    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
345    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
346    /// these keys.
347    ///
348    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
349    #[serde_as(deserialize_as = "DefaultOnError")]
350    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
351    #[serde(default)]
352    #[serde(rename = "_meta")]
353    pub meta: Option<Meta>,
354}
355
356impl TextResourceContents {
357    /// Builds [`TextResourceContents`] with its required content payload; optional annotations and metadata start unset.
358    #[must_use]
359    pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
360        Self {
361            mime_type: None,
362            text: text.into(),
363            uri: uri.into(),
364            meta: None,
365        }
366    }
367
368    /// Sets or clears the optional `mimeType` field.
369    #[must_use]
370    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
371        self.mime_type = mime_type.into_option();
372        self
373    }
374
375    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
376    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
377    /// these keys.
378    ///
379    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
380    #[must_use]
381    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
382        self.meta = meta.into_option();
383        self
384    }
385}
386
387/// Binary resource contents.
388#[serde_as]
389#[skip_serializing_none]
390#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
391#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
392#[serde(rename_all = "camelCase")]
393#[non_exhaustive]
394pub struct BlobResourceContents {
395    /// Base64-encoded bytes for a binary resource payload.
396    pub blob: String,
397    /// MIME type describing the encoded media payload.
398    #[serde_as(deserialize_as = "DefaultOnError")]
399    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
400    #[serde(default)]
401    pub mime_type: Option<String>,
402    /// URI associated with this resource or media payload.
403    pub uri: String,
404    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
405    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
406    /// these keys.
407    ///
408    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
409    #[serde_as(deserialize_as = "DefaultOnError")]
410    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
411    #[serde(default)]
412    #[serde(rename = "_meta")]
413    pub meta: Option<Meta>,
414}
415
416impl BlobResourceContents {
417    /// Builds [`BlobResourceContents`] with its required content payload; optional annotations and metadata start unset.
418    #[must_use]
419    pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
420        Self {
421            blob: blob.into(),
422            mime_type: None,
423            uri: uri.into(),
424            meta: None,
425        }
426    }
427
428    /// Sets or clears the optional `mimeType` field.
429    #[must_use]
430    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
431        self.mime_type = mime_type.into_option();
432        self
433    }
434
435    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
436    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
437    /// these keys.
438    ///
439    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
440    #[must_use]
441    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
442        self.meta = meta.into_option();
443        self
444    }
445}
446
447/// A resource that the server is capable of reading, included in a prompt or tool call result.
448#[serde_as]
449#[skip_serializing_none]
450#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
451#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
452#[serde(rename_all = "camelCase")]
453#[non_exhaustive]
454pub struct ResourceLink {
455    /// Optional annotations that help clients decide how to display or route this content.
456    #[serde_as(deserialize_as = "DefaultOnError")]
457    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
458    #[serde(default)]
459    pub annotations: Option<Annotations>,
460    /// Optional human-readable details shown with this protocol object.
461    #[serde_as(deserialize_as = "DefaultOnError")]
462    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
463    #[serde(default)]
464    pub description: Option<String>,
465    /// MIME type describing the encoded media payload.
466    #[serde_as(deserialize_as = "DefaultOnError")]
467    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
468    #[serde(default)]
469    pub mime_type: Option<String>,
470    /// Human-readable name shown for this protocol object.
471    pub name: String,
472    /// Optional size of the linked resource in bytes, if known.
473    #[serde_as(deserialize_as = "DefaultOnError")]
474    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
475    #[serde(default)]
476    pub size: Option<i64>,
477    /// Optional display title for end-user UI.
478    #[serde_as(deserialize_as = "DefaultOnError")]
479    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
480    #[serde(default)]
481    pub title: Option<String>,
482    /// URI associated with this resource or media payload.
483    pub uri: String,
484    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
485    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
486    /// these keys.
487    ///
488    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
489    #[serde_as(deserialize_as = "DefaultOnError")]
490    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
491    #[serde(default)]
492    #[serde(rename = "_meta")]
493    pub meta: Option<Meta>,
494}
495
496impl ResourceLink {
497    /// Builds [`ResourceLink`] with its required content payload; optional annotations and metadata start unset.
498    #[must_use]
499    pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
500        Self {
501            annotations: None,
502            description: None,
503            mime_type: None,
504            name: name.into(),
505            size: None,
506            title: None,
507            uri: uri.into(),
508            meta: None,
509        }
510    }
511
512    /// Sets or clears the optional `annotations` field.
513    #[must_use]
514    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
515        self.annotations = annotations.into_option();
516        self
517    }
518
519    /// Sets or clears the optional `description` field.
520    #[must_use]
521    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
522        self.description = description.into_option();
523        self
524    }
525
526    /// Sets or clears the optional `mimeType` field.
527    #[must_use]
528    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
529        self.mime_type = mime_type.into_option();
530        self
531    }
532
533    /// Sets or clears the optional `size` field.
534    #[must_use]
535    pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
536        self.size = size.into_option();
537        self
538    }
539
540    /// Sets or clears the optional `title` field.
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    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
548    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
549    /// these keys.
550    ///
551    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
552    #[must_use]
553    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
554        self.meta = meta.into_option();
555        self
556    }
557}
558
559/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
560#[serde_as]
561#[skip_serializing_none]
562#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
563#[derive(Debug, Clone, Deserialize, Serialize, Default)]
564#[serde(rename_all = "camelCase")]
565#[non_exhaustive]
566pub struct Annotations {
567    /// Intended recipients for this content, such as the user or assistant.
568    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
569    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
570    #[serde(default)]
571    pub audience: Option<Vec<Role>>,
572    /// Timestamp indicating when the underlying resource was last modified.
573    #[serde_as(deserialize_as = "DefaultOnError")]
574    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
575    #[serde(default)]
576    pub last_modified: Option<String>,
577    /// Relative importance of this content when clients choose what to surface.
578    #[serde_as(deserialize_as = "DefaultOnError")]
579    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
580    #[serde(default)]
581    pub priority: Option<f64>,
582    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
583    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
584    /// these keys.
585    ///
586    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
587    #[serde_as(deserialize_as = "DefaultOnError")]
588    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
589    #[serde(default)]
590    #[serde(rename = "_meta")]
591    pub meta: Option<Meta>,
592}
593
594/// Priorities are compared with [`f64::total_cmp`] (the IEEE 754 total order), so
595/// equality is reflexive even for NaN and annotations, and the content that
596/// carries them, can be `Eq`. Unlike `==`, it treats `0.0` and `-0.0` as
597/// different, matching how they serialize.
598impl PartialEq for Annotations {
599    fn eq(&self, other: &Self) -> bool {
600        let Self {
601            audience,
602            last_modified,
603            priority,
604            meta,
605        } = self;
606        *audience == other.audience
607            && *last_modified == other.last_modified
608            && priority_eq(*priority, other.priority)
609            && *meta == other.meta
610    }
611}
612
613impl Eq for Annotations {}
614
615fn priority_eq(a: Option<f64>, b: Option<f64>) -> bool {
616    match (a, b) {
617        (Some(a), Some(b)) => a.total_cmp(&b).is_eq(),
618        (None, None) => true,
619        _ => false,
620    }
621}
622
623impl Annotations {
624    /// Creates annotations with no audience, priority, or timestamp hints set.
625    #[must_use]
626    pub fn new() -> Self {
627        Self::default()
628    }
629
630    /// Sets or clears the optional `audience` field.
631    #[must_use]
632    pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
633        self.audience = audience.into_option();
634        self
635    }
636
637    /// Sets or clears the optional `lastModified` field.
638    #[must_use]
639    pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
640        self.last_modified = last_modified.into_option();
641        self
642    }
643
644    /// Sets or clears the optional `priority` field.
645    #[must_use]
646    pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
647        self.priority = priority.into_option();
648        self
649    }
650
651    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
652    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
653    /// these keys.
654    ///
655    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
656    #[must_use]
657    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
658        self.meta = meta.into_option();
659        self
660    }
661}
662
663/// The sender or recipient of messages and data in a conversation.
664#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
665#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
666#[serde(rename_all = "camelCase")]
667#[non_exhaustive]
668pub enum Role {
669    /// The assistant side of a conversation.
670    Assistant,
671    /// The user side of a conversation.
672    User,
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678
679    #[test]
680    fn test_annotations_equality_is_reflexive() {
681        let nan = Annotations::new().priority(f64::NAN);
682        assert_eq!(nan, nan.clone());
683        assert_eq!(
684            Annotations::new().priority(0.5),
685            Annotations::new().priority(0.5)
686        );
687        assert_ne!(
688            Annotations::new().priority(0.0),
689            Annotations::new().priority(-0.0)
690        );
691        assert_ne!(
692            serde_json::to_string(&Annotations::new().priority(0.0)).unwrap(),
693            serde_json::to_string(&Annotations::new().priority(-0.0)).unwrap()
694        );
695        assert_ne!(
696            Annotations::new().priority(0.5),
697            Annotations::new().priority(1.0)
698        );
699        assert_ne!(nan, Annotations::new().priority(0.5));
700        assert_ne!(Annotations::new().priority(0.5), Annotations::new());
701        assert_ne!(
702            Annotations::new().priority(0.5).audience(vec![Role::User]),
703            Annotations::new().priority(0.5)
704        );
705    }
706
707    #[test]
708    fn test_text_content_roundtrip() {
709        let content = TextContent::new("hello world");
710        let json = serde_json::to_value(&content).unwrap();
711        let parsed: TextContent = serde_json::from_value(json).unwrap();
712        assert_eq!(content, parsed);
713    }
714
715    #[test]
716    fn test_text_content_omits_optional_fields() {
717        let content = TextContent::new("hello");
718        let json = serde_json::to_value(&content).unwrap();
719        assert!(!json.as_object().unwrap().contains_key("annotations"));
720        assert!(!json.as_object().unwrap().contains_key("meta"));
721    }
722
723    #[test]
724    fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
725        let missing: TextContent = serde_json::from_value(serde_json::json!({
726            "text": "hello"
727        }))
728        .unwrap();
729        assert_eq!(missing.meta, None);
730
731        let malformed: TextContent = serde_json::from_value(serde_json::json!({
732            "text": "hello",
733            "_meta": false
734        }))
735        .unwrap();
736        assert_eq!(malformed.meta, None);
737    }
738
739    #[test]
740    fn test_text_content_from_string() {
741        let block: ContentBlock = "hello".into();
742        match block {
743            ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
744            _ => panic!("Expected Text variant"),
745        }
746    }
747
748    #[test]
749    fn test_image_content_roundtrip() {
750        let content = ImageContent::new("base64data", "image/png");
751        let json = serde_json::to_value(&content).unwrap();
752        let parsed: ImageContent = serde_json::from_value(json).unwrap();
753        assert_eq!(content, parsed);
754    }
755
756    #[test]
757    fn test_image_content_omits_optional_fields() {
758        let content = ImageContent::new("data", "image/png");
759        let json = serde_json::to_value(&content).unwrap();
760        assert!(!json.as_object().unwrap().contains_key("uri"));
761        assert!(!json.as_object().unwrap().contains_key("annotations"));
762        assert!(!json.as_object().unwrap().contains_key("meta"));
763    }
764
765    #[test]
766    fn test_image_content_with_uri() {
767        let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
768        let json = serde_json::to_value(&content).unwrap();
769        assert_eq!(json["uri"], "https://example.com/image.png");
770    }
771
772    #[test]
773    fn test_audio_content_roundtrip() {
774        let content = AudioContent::new("base64audio", "audio/mp3");
775        let json = serde_json::to_value(&content).unwrap();
776        let parsed: AudioContent = serde_json::from_value(json).unwrap();
777        assert_eq!(content, parsed);
778    }
779
780    #[test]
781    fn test_audio_content_omits_optional_fields() {
782        let content = AudioContent::new("data", "audio/mp3");
783        let json = serde_json::to_value(&content).unwrap();
784        assert!(!json.as_object().unwrap().contains_key("annotations"));
785        assert!(!json.as_object().unwrap().contains_key("meta"));
786    }
787}