Skip to main content

agent_client_protocol_schema/
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 schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
15
16use crate::{IntoOption, Meta, SkipListener};
17
18/// Content blocks represent displayable information in the Agent Client Protocol.
19///
20/// They provide a structured way to handle various types of user-facing content—whether
21/// it's text from language models, images for analysis, or embedded resources for context.
22///
23/// Content blocks appear in:
24/// - User prompts sent via `session/prompt`
25/// - Language model output streamed through `session/update` notifications
26/// - Progress updates and results from tool calls
27///
28/// This structure is compatible with the Model Context Protocol (MCP), enabling
29/// agents to seamlessly forward content from MCP tool outputs without transformation.
30///
31/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
32#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
33#[serde(tag = "type", rename_all = "snake_case")]
34#[schemars(extend("discriminator" = {"propertyName": "type"}))]
35#[non_exhaustive]
36pub enum ContentBlock {
37    /// Text content. May be plain text or formatted with Markdown.
38    ///
39    /// All agents MUST support text content blocks in prompts.
40    /// Clients SHOULD render this text as Markdown.
41    Text(TextContent),
42    /// Images for visual context or analysis.
43    ///
44    /// Requires the `image` prompt capability when included in prompts.
45    Image(ImageContent),
46    /// Audio data for transcription or analysis.
47    ///
48    /// Requires the `audio` prompt capability when included in prompts.
49    Audio(AudioContent),
50    /// References to resources that the agent can access.
51    ///
52    /// All agents MUST support resource links in prompts.
53    ResourceLink(ResourceLink),
54    /// Complete resource contents embedded directly in the message.
55    ///
56    /// Preferred for including context as it avoids extra round-trips.
57    ///
58    /// Requires the `embeddedContext` prompt capability when included in prompts.
59    Resource(EmbeddedResource),
60}
61
62/// Text provided to or from an LLM.
63#[serde_as]
64#[skip_serializing_none]
65#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
66#[non_exhaustive]
67pub struct TextContent {
68    #[serde_as(deserialize_as = "DefaultOnError")]
69    #[serde(default)]
70    pub annotations: Option<Annotations>,
71    pub text: String,
72    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
73    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
74    /// these keys.
75    ///
76    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
77    #[serde(rename = "_meta")]
78    pub meta: Option<Meta>,
79}
80
81impl TextContent {
82    #[must_use]
83    pub fn new(text: impl Into<String>) -> Self {
84        Self {
85            annotations: None,
86            text: text.into(),
87            meta: None,
88        }
89    }
90
91    #[must_use]
92    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
93        self.annotations = annotations.into_option();
94        self
95    }
96
97    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
98    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
99    /// these keys.
100    ///
101    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
102    #[must_use]
103    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
104        self.meta = meta.into_option();
105        self
106    }
107}
108
109impl<T: Into<String>> From<T> for ContentBlock {
110    fn from(value: T) -> Self {
111        Self::Text(TextContent::new(value))
112    }
113}
114
115/// An image provided to or from an LLM.
116#[serde_as]
117#[skip_serializing_none]
118#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
119#[serde(rename_all = "camelCase")]
120#[non_exhaustive]
121pub struct ImageContent {
122    #[serde_as(deserialize_as = "DefaultOnError")]
123    #[serde(default)]
124    pub annotations: Option<Annotations>,
125    pub data: String,
126    pub mime_type: String,
127    pub uri: Option<String>,
128    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
129    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
130    /// these keys.
131    ///
132    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
133    #[serde(rename = "_meta")]
134    pub meta: Option<Meta>,
135}
136
137impl ImageContent {
138    #[must_use]
139    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
140        Self {
141            annotations: None,
142            data: data.into(),
143            mime_type: mime_type.into(),
144            uri: None,
145            meta: None,
146        }
147    }
148
149    #[must_use]
150    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
151        self.annotations = annotations.into_option();
152        self
153    }
154
155    #[must_use]
156    pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
157        self.uri = uri.into_option();
158        self
159    }
160
161    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
162    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
163    /// these keys.
164    ///
165    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
166    #[must_use]
167    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
168        self.meta = meta.into_option();
169        self
170    }
171}
172
173/// Audio provided to or from an LLM.
174#[serde_as]
175#[skip_serializing_none]
176#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
177#[serde(rename_all = "camelCase")]
178#[non_exhaustive]
179pub struct AudioContent {
180    #[serde_as(deserialize_as = "DefaultOnError")]
181    #[serde(default)]
182    pub annotations: Option<Annotations>,
183    pub data: String,
184    pub mime_type: String,
185    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
186    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
187    /// these keys.
188    ///
189    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
190    #[serde(rename = "_meta")]
191    pub meta: Option<Meta>,
192}
193
194impl AudioContent {
195    #[must_use]
196    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
197        Self {
198            annotations: None,
199            data: data.into(),
200            mime_type: mime_type.into(),
201            meta: None,
202        }
203    }
204
205    #[must_use]
206    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
207        self.annotations = annotations.into_option();
208        self
209    }
210
211    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
212    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
213    /// these keys.
214    ///
215    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
216    #[must_use]
217    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
218        self.meta = meta.into_option();
219        self
220    }
221}
222
223/// The contents of a resource, embedded into a prompt or tool call result.
224#[serde_as]
225#[skip_serializing_none]
226#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
227#[non_exhaustive]
228pub struct EmbeddedResource {
229    #[serde_as(deserialize_as = "DefaultOnError")]
230    #[serde(default)]
231    pub annotations: Option<Annotations>,
232    pub resource: EmbeddedResourceResource,
233    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
234    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
235    /// these keys.
236    ///
237    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
238    #[serde(rename = "_meta")]
239    pub meta: Option<Meta>,
240}
241
242impl EmbeddedResource {
243    #[must_use]
244    pub fn new(resource: EmbeddedResourceResource) -> Self {
245        Self {
246            annotations: None,
247            resource,
248            meta: None,
249        }
250    }
251
252    #[must_use]
253    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
254        self.annotations = annotations.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/// Resource content that can be embedded in a message.
271#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
272#[serde(untagged)]
273#[non_exhaustive]
274pub enum EmbeddedResourceResource {
275    TextResourceContents(TextResourceContents),
276    BlobResourceContents(BlobResourceContents),
277}
278
279/// Text-based resource contents.
280#[skip_serializing_none]
281#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
282#[serde(rename_all = "camelCase")]
283#[non_exhaustive]
284pub struct TextResourceContents {
285    pub mime_type: Option<String>,
286    pub text: String,
287    pub uri: String,
288    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
289    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
290    /// these keys.
291    ///
292    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
293    #[serde(rename = "_meta")]
294    pub meta: Option<Meta>,
295}
296
297impl TextResourceContents {
298    #[must_use]
299    pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
300        Self {
301            mime_type: None,
302            text: text.into(),
303            uri: uri.into(),
304            meta: None,
305        }
306    }
307
308    #[must_use]
309    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
310        self.mime_type = mime_type.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/// Binary resource contents.
327#[skip_serializing_none]
328#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
329#[serde(rename_all = "camelCase")]
330#[non_exhaustive]
331pub struct BlobResourceContents {
332    pub blob: String,
333    pub mime_type: Option<String>,
334    pub uri: String,
335    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
336    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
337    /// these keys.
338    ///
339    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
340    #[serde(rename = "_meta")]
341    pub meta: Option<Meta>,
342}
343
344impl BlobResourceContents {
345    #[must_use]
346    pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
347        Self {
348            blob: blob.into(),
349            mime_type: None,
350            uri: uri.into(),
351            meta: None,
352        }
353    }
354
355    #[must_use]
356    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
357        self.mime_type = mime_type.into_option();
358        self
359    }
360
361    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
362    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
363    /// these keys.
364    ///
365    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
366    #[must_use]
367    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
368        self.meta = meta.into_option();
369        self
370    }
371}
372
373/// A resource that the server is capable of reading, included in a prompt or tool call result.
374#[serde_as]
375#[skip_serializing_none]
376#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
377#[serde(rename_all = "camelCase")]
378#[non_exhaustive]
379pub struct ResourceLink {
380    #[serde_as(deserialize_as = "DefaultOnError")]
381    #[serde(default)]
382    pub annotations: Option<Annotations>,
383    pub description: Option<String>,
384    pub mime_type: Option<String>,
385    pub name: String,
386    pub size: Option<i64>,
387    pub title: Option<String>,
388    pub uri: String,
389    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
390    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
391    /// these keys.
392    ///
393    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
394    #[serde(rename = "_meta")]
395    pub meta: Option<Meta>,
396}
397
398impl ResourceLink {
399    #[must_use]
400    pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
401        Self {
402            annotations: None,
403            description: None,
404            mime_type: None,
405            name: name.into(),
406            size: None,
407            title: None,
408            uri: uri.into(),
409            meta: None,
410        }
411    }
412
413    #[must_use]
414    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
415        self.annotations = annotations.into_option();
416        self
417    }
418
419    #[must_use]
420    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
421        self.description = description.into_option();
422        self
423    }
424
425    #[must_use]
426    pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
427        self.mime_type = mime_type.into_option();
428        self
429    }
430
431    #[must_use]
432    pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
433        self.size = size.into_option();
434        self
435    }
436
437    #[must_use]
438    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
439        self.title = title.into_option();
440        self
441    }
442
443    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
444    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
445    /// these keys.
446    ///
447    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
448    #[must_use]
449    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
450        self.meta = meta.into_option();
451        self
452    }
453}
454
455/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
456#[serde_as]
457#[skip_serializing_none]
458#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
459#[serde(rename_all = "camelCase")]
460#[non_exhaustive]
461pub struct Annotations {
462    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
463    #[serde(default)]
464    pub audience: Option<Vec<Role>>,
465    pub last_modified: Option<String>,
466    pub priority: Option<f64>,
467    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
468    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
469    /// these keys.
470    ///
471    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
472    #[serde(rename = "_meta")]
473    pub meta: Option<Meta>,
474}
475
476impl Annotations {
477    #[must_use]
478    pub fn new() -> Self {
479        Self::default()
480    }
481
482    #[must_use]
483    pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
484        self.audience = audience.into_option();
485        self
486    }
487
488    #[must_use]
489    pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
490        self.last_modified = last_modified.into_option();
491        self
492    }
493
494    #[must_use]
495    pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
496        self.priority = priority.into_option();
497        self
498    }
499
500    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
501    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
502    /// these keys.
503    ///
504    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
505    #[must_use]
506    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
507        self.meta = meta.into_option();
508        self
509    }
510}
511
512/// The sender or recipient of messages and data in a conversation.
513#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
514#[serde(rename_all = "camelCase")]
515#[non_exhaustive]
516pub enum Role {
517    Assistant,
518    User,
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_text_content_roundtrip() {
527        let content = TextContent::new("hello world");
528        let json = serde_json::to_value(&content).unwrap();
529        let parsed: TextContent = serde_json::from_value(json).unwrap();
530        assert_eq!(content, parsed);
531    }
532
533    #[test]
534    fn test_text_content_omits_optional_fields() {
535        let content = TextContent::new("hello");
536        let json = serde_json::to_value(&content).unwrap();
537        assert!(!json.as_object().unwrap().contains_key("annotations"));
538        assert!(!json.as_object().unwrap().contains_key("meta"));
539    }
540
541    #[test]
542    fn test_text_content_from_string() {
543        let block: ContentBlock = "hello".into();
544        match block {
545            ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
546            _ => panic!("Expected Text variant"),
547        }
548    }
549
550    #[test]
551    fn test_image_content_roundtrip() {
552        let content = ImageContent::new("base64data", "image/png");
553        let json = serde_json::to_value(&content).unwrap();
554        let parsed: ImageContent = serde_json::from_value(json).unwrap();
555        assert_eq!(content, parsed);
556    }
557
558    #[test]
559    fn test_image_content_omits_optional_fields() {
560        let content = ImageContent::new("data", "image/png");
561        let json = serde_json::to_value(&content).unwrap();
562        assert!(!json.as_object().unwrap().contains_key("uri"));
563        assert!(!json.as_object().unwrap().contains_key("annotations"));
564        assert!(!json.as_object().unwrap().contains_key("meta"));
565    }
566
567    #[test]
568    fn test_image_content_with_uri() {
569        let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
570        let json = serde_json::to_value(&content).unwrap();
571        assert_eq!(json["uri"], "https://example.com/image.png");
572    }
573
574    #[test]
575    fn test_audio_content_roundtrip() {
576        let content = AudioContent::new("base64audio", "audio/mp3");
577        let json = serde_json::to_value(&content).unwrap();
578        let parsed: AudioContent = serde_json::from_value(json).unwrap();
579        assert_eq!(content, parsed);
580    }
581
582    #[test]
583    fn test_audio_content_omits_optional_fields() {
584        let content = AudioContent::new("data", "audio/mp3");
585        let json = serde_json::to_value(&content).unwrap();
586        assert!(!json.as_object().unwrap().contains_key("annotations"));
587        assert!(!json.as_object().unwrap().contains_key("meta"));
588    }
589}