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