claude_agent/types/content/
mod.rs

1//! Content block types for messages.
2
3mod image;
4mod server_tools;
5mod tool_blocks;
6
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11pub use image::ImageSource;
12pub use server_tools::{
13    ServerToolUseBlock, WebFetchResultItem, WebFetchToolResultBlock, WebFetchToolResultContent,
14    WebFetchToolResultError, WebSearchResultItem, WebSearchToolResultBlock,
15    WebSearchToolResultContent, WebSearchToolResultError,
16};
17pub use tool_blocks::{ToolResultBlock, ToolResultContent, ToolResultContentBlock, ToolUseBlock};
18
19use super::citations::Citation;
20use super::document::DocumentBlock;
21use super::message::CacheControl;
22use super::search::SearchResultBlock;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(tag = "type", rename_all = "snake_case")]
26pub enum ContentBlock {
27    Text {
28        text: String,
29        #[serde(skip_serializing_if = "Option::is_none")]
30        citations: Option<Vec<Citation>>,
31        #[serde(skip_serializing_if = "Option::is_none")]
32        cache_control: Option<CacheControl>,
33    },
34    Image {
35        source: ImageSource,
36    },
37    Document(DocumentBlock),
38    #[serde(rename = "search_result")]
39    SearchResult(SearchResultBlock),
40    #[serde(rename = "tool_use")]
41    ToolUse(ToolUseBlock),
42    #[serde(rename = "tool_result")]
43    ToolResult(ToolResultBlock),
44    Thinking(ThinkingBlock),
45    #[serde(rename = "redacted_thinking")]
46    RedactedThinking {
47        data: String,
48    },
49    #[serde(rename = "server_tool_use")]
50    ServerToolUse(ServerToolUseBlock),
51    #[serde(rename = "web_search_tool_result")]
52    WebSearchToolResult(WebSearchToolResultBlock),
53    #[serde(rename = "web_fetch_tool_result")]
54    WebFetchToolResult(WebFetchToolResultBlock),
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ThinkingBlock {
59    pub thinking: String,
60    pub signature: String,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct TextBlock {
65    pub text: String,
66}
67
68impl From<&str> for ContentBlock {
69    fn from(text: &str) -> Self {
70        ContentBlock::Text {
71            text: text.to_string(),
72            citations: None,
73            cache_control: None,
74        }
75    }
76}
77
78impl From<String> for ContentBlock {
79    fn from(text: String) -> Self {
80        ContentBlock::Text {
81            text,
82            citations: None,
83            cache_control: None,
84        }
85    }
86}
87
88impl From<DocumentBlock> for ContentBlock {
89    fn from(doc: DocumentBlock) -> Self {
90        ContentBlock::Document(doc)
91    }
92}
93
94impl From<SearchResultBlock> for ContentBlock {
95    fn from(result: SearchResultBlock) -> Self {
96        ContentBlock::SearchResult(result)
97    }
98}
99
100impl ContentBlock {
101    pub fn text(text: impl Into<String>) -> Self {
102        ContentBlock::Text {
103            text: text.into(),
104            citations: None,
105            cache_control: None,
106        }
107    }
108
109    pub fn text_cached(text: impl Into<String>) -> Self {
110        ContentBlock::Text {
111            text: text.into(),
112            citations: None,
113            cache_control: Some(CacheControl::ephemeral()),
114        }
115    }
116
117    pub fn text_with_cache(text: impl Into<String>, cache: CacheControl) -> Self {
118        ContentBlock::Text {
119            text: text.into(),
120            citations: None,
121            cache_control: Some(cache),
122        }
123    }
124
125    pub fn text_with_citations(text: impl Into<String>, citations: Vec<Citation>) -> Self {
126        ContentBlock::Text {
127            text: text.into(),
128            citations: if citations.is_empty() {
129                None
130            } else {
131                Some(citations)
132            },
133            cache_control: None,
134        }
135    }
136
137    pub fn document(doc: DocumentBlock) -> Self {
138        ContentBlock::Document(doc)
139    }
140
141    pub fn search_result(result: SearchResultBlock) -> Self {
142        ContentBlock::SearchResult(result)
143    }
144
145    pub fn image(source: ImageSource) -> Self {
146        ContentBlock::Image { source }
147    }
148
149    pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
150        ContentBlock::Image {
151            source: ImageSource::base64(media_type, data),
152        }
153    }
154
155    pub fn image_url(url: impl Into<String>) -> Self {
156        ContentBlock::Image {
157            source: ImageSource::from_url(url),
158        }
159    }
160
161    pub fn image_file(file_id: impl Into<String>) -> Self {
162        ContentBlock::Image {
163            source: ImageSource::from_file(file_id),
164        }
165    }
166
167    pub async fn image_from_path(path: impl AsRef<Path>) -> crate::Result<Self> {
168        Ok(ContentBlock::Image {
169            source: ImageSource::from_path(path).await?,
170        })
171    }
172
173    pub fn as_text(&self) -> Option<&str> {
174        match self {
175            ContentBlock::Text { text, .. } => Some(text),
176            _ => None,
177        }
178    }
179
180    pub fn citations(&self) -> Option<&[Citation]> {
181        match self {
182            ContentBlock::Text { citations, .. } => citations.as_deref(),
183            _ => None,
184        }
185    }
186
187    pub fn has_citations(&self) -> bool {
188        matches!(self, ContentBlock::Text { citations: Some(c), .. } if !c.is_empty())
189    }
190
191    pub fn cache_control(&self) -> Option<&CacheControl> {
192        match self {
193            ContentBlock::Text { cache_control, .. } => cache_control.as_ref(),
194            ContentBlock::Document(doc) => doc.cache_control.as_ref(),
195            ContentBlock::SearchResult(sr) => sr.cache_control.as_ref(),
196            _ => None,
197        }
198    }
199
200    pub fn is_cached(&self) -> bool {
201        self.cache_control().is_some()
202    }
203
204    /// Set cache control in-place (only for Text blocks).
205    pub fn set_cache_control(&mut self, cache: Option<CacheControl>) {
206        if let ContentBlock::Text { cache_control, .. } = self {
207            *cache_control = cache;
208        }
209    }
210
211    pub fn with_cache_control(self, cache: CacheControl) -> Self {
212        match self {
213            ContentBlock::Text {
214                text, citations, ..
215            } => ContentBlock::Text {
216                text,
217                citations,
218                cache_control: Some(cache),
219            },
220            other => other,
221        }
222    }
223
224    pub fn without_cache_control(self) -> Self {
225        match self {
226            ContentBlock::Text {
227                text, citations, ..
228            } => ContentBlock::Text {
229                text,
230                citations,
231                cache_control: None,
232            },
233            other => other,
234        }
235    }
236
237    pub fn as_document(&self) -> Option<&DocumentBlock> {
238        match self {
239            ContentBlock::Document(doc) => Some(doc),
240            _ => None,
241        }
242    }
243
244    pub fn as_search_result(&self) -> Option<&SearchResultBlock> {
245        match self {
246            ContentBlock::SearchResult(sr) => Some(sr),
247            _ => None,
248        }
249    }
250
251    pub fn is_document(&self) -> bool {
252        matches!(self, ContentBlock::Document(_))
253    }
254
255    pub fn is_search_result(&self) -> bool {
256        matches!(self, ContentBlock::SearchResult(_))
257    }
258
259    pub fn is_image(&self) -> bool {
260        matches!(self, ContentBlock::Image { .. })
261    }
262
263    pub fn as_image(&self) -> Option<&ImageSource> {
264        match self {
265            ContentBlock::Image { source } => Some(source),
266            _ => None,
267        }
268    }
269
270    pub fn as_thinking(&self) -> Option<&ThinkingBlock> {
271        match self {
272            ContentBlock::Thinking(block) => Some(block),
273            _ => None,
274        }
275    }
276
277    pub fn is_thinking(&self) -> bool {
278        matches!(
279            self,
280            ContentBlock::Thinking(_) | ContentBlock::RedactedThinking { .. }
281        )
282    }
283
284    pub fn is_server_tool_use(&self) -> bool {
285        matches!(self, ContentBlock::ServerToolUse(_))
286    }
287
288    pub fn as_server_tool_use(&self) -> Option<&ServerToolUseBlock> {
289        match self {
290            ContentBlock::ServerToolUse(block) => Some(block),
291            _ => None,
292        }
293    }
294
295    pub fn is_web_search_result(&self) -> bool {
296        matches!(self, ContentBlock::WebSearchToolResult(_))
297    }
298
299    pub fn as_web_search_result(&self) -> Option<&WebSearchToolResultBlock> {
300        match self {
301            ContentBlock::WebSearchToolResult(block) => Some(block),
302            _ => None,
303        }
304    }
305
306    pub fn is_web_fetch_result(&self) -> bool {
307        matches!(self, ContentBlock::WebFetchToolResult(_))
308    }
309
310    pub fn as_web_fetch_result(&self) -> Option<&WebFetchToolResultBlock> {
311        match self {
312            ContentBlock::WebFetchToolResult(block) => Some(block),
313            _ => None,
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_content_block_text() {
324        let block = ContentBlock::text("Hello");
325        assert_eq!(block.as_text(), Some("Hello"));
326        assert!(!block.has_citations());
327        assert!(!block.is_cached());
328    }
329
330    #[test]
331    fn test_content_block_cached() {
332        let block = ContentBlock::text_cached("Hello");
333        assert_eq!(block.as_text(), Some("Hello"));
334        assert!(block.is_cached());
335        assert!(block.cache_control().is_some());
336    }
337
338    #[test]
339    fn test_content_block_with_cache_control() {
340        use crate::types::{CacheControl, CacheTtl};
341
342        let block = ContentBlock::text("Hello").with_cache_control(CacheControl::ephemeral_1h());
343        assert!(block.is_cached());
344        assert_eq!(block.cache_control().unwrap().ttl, Some(CacheTtl::OneHour));
345
346        let block = block.without_cache_control();
347        assert!(!block.is_cached());
348    }
349
350    #[test]
351    fn test_content_block_from_document() {
352        let doc = DocumentBlock::text("content");
353        let block: ContentBlock = doc.into();
354        assert!(block.is_document());
355    }
356
357    #[test]
358    fn test_content_block_image() {
359        let block = ContentBlock::image_file("file_123");
360        assert!(block.is_image());
361        assert!(block.as_image().is_some());
362        assert_eq!(block.as_image().unwrap().file_id(), Some("file_123"));
363
364        let block = ContentBlock::image_url("https://example.com/img.png");
365        assert!(block.is_image());
366    }
367
368    #[tokio::test]
369    async fn test_content_block_image_from_path() {
370        let dir = tempfile::tempdir().unwrap();
371        let jpeg_path = dir.path().join("test.jpg");
372
373        let jpeg_data: [u8; 4] = [0xFF, 0xD8, 0xFF, 0xE0];
374        tokio::fs::write(&jpeg_path, &jpeg_data).await.unwrap();
375
376        let block = ContentBlock::image_from_path(&jpeg_path).await.unwrap();
377        assert!(block.is_image());
378
379        let source = block.as_image().unwrap();
380        assert!(source.is_base64());
381        assert_eq!(source.media_type(), Some("image/jpeg"));
382    }
383
384    #[test]
385    fn test_thinking_block_serialization() {
386        let block = ThinkingBlock {
387            thinking: "Let me analyze this...".to_string(),
388            signature: "sig_abc123".to_string(),
389        };
390        let json = serde_json::to_string(&block).unwrap();
391        assert!(json.contains("\"thinking\":\"Let me analyze this...\""));
392        assert!(json.contains("\"signature\":\"sig_abc123\""));
393    }
394
395    #[test]
396    fn test_thinking_block_deserialization() {
397        let json = r#"{"thinking":"Step by step reasoning","signature":"sig_xyz"}"#;
398        let block: ThinkingBlock = serde_json::from_str(json).unwrap();
399        assert_eq!(block.thinking, "Step by step reasoning");
400        assert_eq!(block.signature, "sig_xyz");
401    }
402
403    #[test]
404    fn test_content_block_thinking_variant() {
405        let thinking = ThinkingBlock {
406            thinking: "Analysis".to_string(),
407            signature: "sig".to_string(),
408        };
409        let block = ContentBlock::Thinking(thinking);
410        assert!(block.is_thinking());
411        assert!(block.as_thinking().is_some());
412        assert_eq!(block.as_thinking().unwrap().thinking, "Analysis");
413    }
414
415    #[test]
416    fn test_content_block_redacted_thinking() {
417        let block = ContentBlock::RedactedThinking {
418            data: "encrypted_data".to_string(),
419        };
420        assert!(block.is_thinking());
421        assert!(block.as_thinking().is_none());
422    }
423
424    #[test]
425    fn test_thinking_content_block_serialization() {
426        let block = ContentBlock::Thinking(ThinkingBlock {
427            thinking: "Reasoning here".to_string(),
428            signature: "sig123".to_string(),
429        });
430        let json = serde_json::to_string(&block).unwrap();
431        assert!(json.contains("\"type\":\"thinking\""));
432        assert!(json.contains("\"thinking\":\"Reasoning here\""));
433    }
434
435    #[test]
436    fn test_redacted_thinking_serialization() {
437        let block = ContentBlock::RedactedThinking {
438            data: "redacted_content".to_string(),
439        };
440        let json = serde_json::to_string(&block).unwrap();
441        assert!(json.contains("\"type\":\"redacted_thinking\""));
442        assert!(json.contains("\"data\":\"redacted_content\""));
443    }
444
445    #[test]
446    fn test_content_block_server_tool_helpers() {
447        let text_block = ContentBlock::text("Hello");
448        assert!(!text_block.is_server_tool_use());
449        assert!(!text_block.is_web_search_result());
450        assert!(!text_block.is_web_fetch_result());
451        assert!(text_block.as_server_tool_use().is_none());
452        assert!(text_block.as_web_search_result().is_none());
453        assert!(text_block.as_web_fetch_result().is_none());
454    }
455}