Skip to main content

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