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