Skip to main content

claude_codes/io/
content_blocks.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5/// Deserialize content blocks that can be either a string or array
6pub(crate) fn deserialize_content_blocks<'de, D>(
7    deserializer: D,
8) -> Result<Vec<ContentBlock>, D::Error>
9where
10    D: Deserializer<'de>,
11{
12    let value: Value = Value::deserialize(deserializer)?;
13    match value {
14        Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock {
15            text: s,
16            citations: Vec::new(),
17        })]),
18        Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
19        _ => Err(serde::de::Error::custom(
20            "content must be a string or array",
21        )),
22    }
23}
24
25/// Content blocks for messages
26///
27/// Includes typed variants for known block types and an `Unknown` fallback
28/// for forward compatibility with new block types added by the CLI.
29#[derive(Debug, Clone)]
30pub enum ContentBlock {
31    Text(TextBlock),
32    Image(ImageBlock),
33    Thinking(ThinkingBlock),
34    ToolUse(ToolUseBlock),
35    ToolResult(ToolResultBlock),
36    /// Server-side tool use (e.g., web search, code execution).
37    ServerToolUse(ServerToolUseBlock),
38    /// Result from a web search server tool.
39    WebSearchToolResult(WebSearchToolResultBlock),
40    /// Result from server-side code execution.
41    CodeExecutionToolResult(CodeExecutionToolResultBlock),
42    /// MCP tool invocation as a content block.
43    McpToolUse(McpToolUseBlock),
44    /// MCP tool result as a content block.
45    McpToolResult(McpToolResultBlock),
46    /// Container file upload content block.
47    ContainerUpload(ContainerUploadBlock),
48    /// A content block type not yet known to this version of the crate.
49    /// Contains the raw JSON value for caller inspection.
50    Unknown(Value),
51}
52
53impl ContentBlock {
54    /// Returns the type tag string for this content block.
55    pub fn block_type(&self) -> &str {
56        match self {
57            Self::Text(_) => "text",
58            Self::Image(_) => "image",
59            Self::Thinking(_) => "thinking",
60            Self::ToolUse(_) => "tool_use",
61            Self::ToolResult(_) => "tool_result",
62            Self::ServerToolUse(_) => "server_tool_use",
63            Self::WebSearchToolResult(_) => "web_search_tool_result",
64            Self::CodeExecutionToolResult(_) => "code_execution_tool_result",
65            Self::McpToolUse(_) => "mcp_tool_use",
66            Self::McpToolResult(_) => "mcp_tool_result",
67            Self::ContainerUpload(_) => "container_upload",
68            Self::Unknown(v) => v.get("type").and_then(|t| t.as_str()).unwrap_or("unknown"),
69        }
70    }
71
72    /// Returns `true` if this is an unknown/unrecognized content block type.
73    pub fn is_unknown(&self) -> bool {
74        matches!(self, Self::Unknown(_))
75    }
76}
77
78impl Serialize for ContentBlock {
79    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
80        match self {
81            Self::Text(v) => serialize_tagged("text", v, serializer),
82            Self::Image(v) => serialize_tagged("image", v, serializer),
83            Self::Thinking(v) => serialize_tagged("thinking", v, serializer),
84            Self::ToolUse(v) => serialize_tagged("tool_use", v, serializer),
85            Self::ToolResult(v) => serialize_tagged("tool_result", v, serializer),
86            Self::ServerToolUse(v) => serialize_tagged("server_tool_use", v, serializer),
87            Self::WebSearchToolResult(v) => {
88                serialize_tagged("web_search_tool_result", v, serializer)
89            }
90            Self::CodeExecutionToolResult(v) => {
91                serialize_tagged("code_execution_tool_result", v, serializer)
92            }
93            Self::McpToolUse(v) => serialize_tagged("mcp_tool_use", v, serializer),
94            Self::McpToolResult(v) => serialize_tagged("mcp_tool_result", v, serializer),
95            Self::ContainerUpload(v) => serialize_tagged("container_upload", v, serializer),
96            Self::Unknown(v) => v.serialize(serializer),
97        }
98    }
99}
100
101impl<'de> Deserialize<'de> for ContentBlock {
102    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103        let value = Value::deserialize(deserializer)?;
104        let type_str = value
105            .get("type")
106            .and_then(|v| v.as_str())
107            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
108
109        match type_str {
110            "text" => serde_json::from_value(value)
111                .map(ContentBlock::Text)
112                .map_err(serde::de::Error::custom),
113            "image" => serde_json::from_value(value)
114                .map(ContentBlock::Image)
115                .map_err(serde::de::Error::custom),
116            "thinking" => serde_json::from_value(value)
117                .map(ContentBlock::Thinking)
118                .map_err(serde::de::Error::custom),
119            "tool_use" => serde_json::from_value(value)
120                .map(ContentBlock::ToolUse)
121                .map_err(serde::de::Error::custom),
122            "tool_result" => serde_json::from_value(value)
123                .map(ContentBlock::ToolResult)
124                .map_err(serde::de::Error::custom),
125            "server_tool_use" => serde_json::from_value(value)
126                .map(ContentBlock::ServerToolUse)
127                .map_err(serde::de::Error::custom),
128            "web_search_tool_result" => serde_json::from_value(value)
129                .map(ContentBlock::WebSearchToolResult)
130                .map_err(serde::de::Error::custom),
131            "code_execution_tool_result" => serde_json::from_value(value)
132                .map(ContentBlock::CodeExecutionToolResult)
133                .map_err(serde::de::Error::custom),
134            "mcp_tool_use" => serde_json::from_value(value)
135                .map(ContentBlock::McpToolUse)
136                .map_err(serde::de::Error::custom),
137            "mcp_tool_result" => serde_json::from_value(value)
138                .map(ContentBlock::McpToolResult)
139                .map_err(serde::de::Error::custom),
140            "container_upload" => serde_json::from_value(value)
141                .map(ContentBlock::ContainerUpload)
142                .map_err(serde::de::Error::custom),
143            _ => Ok(ContentBlock::Unknown(value)),
144        }
145    }
146}
147
148/// Serialize a value with an internally-tagged "type" field.
149fn serialize_tagged<S: Serializer, T: Serialize>(
150    tag: &str,
151    value: &T,
152    serializer: S,
153) -> Result<S::Ok, S::Error> {
154    let mut map = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
155    if let Some(obj) = map.as_object_mut() {
156        obj.insert("type".to_string(), Value::String(tag.to_string()));
157    }
158    map.serialize(serializer)
159}
160
161/// Text content block
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct TextBlock {
164    pub text: String,
165    /// Citations associated with this text block, if any.
166    /// Populated when the model references web search results or other sources.
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    pub citations: Vec<Value>,
169}
170
171/// Image content block (follows Anthropic API structure)
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ImageBlock {
174    pub source: ImageSource,
175}
176
177/// Encoding type for image source data.
178#[derive(Debug, Clone, PartialEq, Eq, Hash)]
179pub enum ImageSourceType {
180    /// Base64-encoded image data.
181    Base64,
182    /// A source type not yet known to this version of the crate.
183    Unknown(String),
184}
185
186impl ImageSourceType {
187    pub fn as_str(&self) -> &str {
188        match self {
189            Self::Base64 => "base64",
190            Self::Unknown(s) => s.as_str(),
191        }
192    }
193}
194
195impl fmt::Display for ImageSourceType {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        f.write_str(self.as_str())
198    }
199}
200
201impl From<&str> for ImageSourceType {
202    fn from(s: &str) -> Self {
203        match s {
204            "base64" => Self::Base64,
205            other => Self::Unknown(other.to_string()),
206        }
207    }
208}
209
210impl Serialize for ImageSourceType {
211    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
212        serializer.serialize_str(self.as_str())
213    }
214}
215
216impl<'de> Deserialize<'de> for ImageSourceType {
217    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
218        let s = String::deserialize(deserializer)?;
219        Ok(Self::from(s.as_str()))
220    }
221}
222
223/// MIME type for image content.
224#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225pub enum MediaType {
226    /// JPEG image.
227    Jpeg,
228    /// PNG image.
229    Png,
230    /// GIF image.
231    Gif,
232    /// WebP image.
233    Webp,
234    /// A media type not yet known to this version of the crate.
235    Unknown(String),
236}
237
238impl MediaType {
239    pub fn as_str(&self) -> &str {
240        match self {
241            Self::Jpeg => "image/jpeg",
242            Self::Png => "image/png",
243            Self::Gif => "image/gif",
244            Self::Webp => "image/webp",
245            Self::Unknown(s) => s.as_str(),
246        }
247    }
248}
249
250impl fmt::Display for MediaType {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        f.write_str(self.as_str())
253    }
254}
255
256impl From<&str> for MediaType {
257    fn from(s: &str) -> Self {
258        match s {
259            "image/jpeg" => Self::Jpeg,
260            "image/png" => Self::Png,
261            "image/gif" => Self::Gif,
262            "image/webp" => Self::Webp,
263            other => Self::Unknown(other.to_string()),
264        }
265    }
266}
267
268impl Serialize for MediaType {
269    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
270        serializer.serialize_str(self.as_str())
271    }
272}
273
274impl<'de> Deserialize<'de> for MediaType {
275    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
276        let s = String::deserialize(deserializer)?;
277        Ok(Self::from(s.as_str()))
278    }
279}
280
281/// Image source information
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct ImageSource {
284    #[serde(rename = "type")]
285    pub source_type: ImageSourceType,
286    pub media_type: MediaType,
287    pub data: String,
288}
289
290/// Thinking content block
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ThinkingBlock {
293    pub thinking: String,
294    pub signature: String,
295}
296
297/// Tool use content block
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct ToolUseBlock {
300    pub id: String,
301    pub name: String,
302    pub input: Value,
303}
304
305impl ToolUseBlock {
306    /// Try to parse the input as a typed ToolInput.
307    ///
308    /// This attempts to deserialize the raw JSON input into a strongly-typed
309    /// `ToolInput` enum variant. Returns `None` if parsing fails.
310    ///
311    /// # Example
312    ///
313    /// ```
314    /// use claude_codes::{ToolUseBlock, ToolInput};
315    /// use serde_json::json;
316    ///
317    /// let block = ToolUseBlock {
318    ///     id: "toolu_123".to_string(),
319    ///     name: "Bash".to_string(),
320    ///     input: json!({"command": "ls -la"}),
321    /// };
322    ///
323    /// if let Some(ToolInput::Bash(bash)) = block.typed_input() {
324    ///     assert_eq!(bash.command, "ls -la");
325    /// }
326    /// ```
327    pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
328        serde_json::from_value(self.input.clone()).ok()
329    }
330
331    /// Parse the input as a typed ToolInput, returning an error on failure.
332    ///
333    /// Unlike `typed_input()`, this method returns the parsing error for debugging.
334    pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
335        serde_json::from_value(self.input.clone())
336    }
337}
338
339/// Tool result content block
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ToolResultBlock {
342    pub tool_use_id: String,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub content: Option<ToolResultContent>,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub is_error: Option<bool>,
347}
348
349/// Tool result content type
350#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(untagged)]
352pub enum ToolResultContent {
353    Text(String),
354    Structured(Vec<Value>),
355}
356
357/// Server-side tool use content block (e.g., web search, code execution).
358///
359/// Emitted when the model invokes an Anthropic-hosted server tool.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ServerToolUseBlock {
362    pub id: String,
363    pub name: String,
364    #[serde(default)]
365    pub input: Value,
366}
367
368/// Result from a web search server tool.
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct WebSearchToolResultBlock {
371    pub tool_use_id: String,
372    #[serde(default)]
373    pub content: Value,
374}
375
376/// Result from server-side code execution.
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct CodeExecutionToolResultBlock {
379    pub tool_use_id: String,
380    #[serde(default)]
381    pub content: Value,
382}
383
384/// MCP tool invocation content block.
385///
386/// Emitted when the model invokes a tool provided by an MCP server.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct McpToolUseBlock {
389    pub id: String,
390    pub name: String,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub server_name: Option<String>,
393    #[serde(default)]
394    pub input: Value,
395}
396
397/// MCP tool result content block.
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct McpToolResultBlock {
400    pub tool_use_id: String,
401    #[serde(default)]
402    pub content: Value,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub is_error: Option<bool>,
405}
406
407/// Container file upload content block.
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ContainerUploadBlock {
410    #[serde(flatten)]
411    pub data: Value,
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use serde_json::json;
418
419    #[test]
420    fn test_unknown_content_block_deserializes() {
421        let json = json!({
422            "type": "some_future_block_type",
423            "data": "arbitrary"
424        });
425
426        let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
427        assert!(block.is_unknown());
428        assert_eq!(block.block_type(), "some_future_block_type");
429        if let ContentBlock::Unknown(v) = &block {
430            assert_eq!(v["data"], "arbitrary");
431        } else {
432            panic!("Expected Unknown variant");
433        }
434    }
435
436    #[test]
437    fn test_unknown_block_roundtrips() {
438        let json = json!({
439            "type": "some_future_type",
440            "tool_use_id": "x",
441            "content": [{"nested": true}]
442        });
443
444        let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
445        let reserialized = serde_json::to_value(&block).unwrap();
446        assert_eq!(json, reserialized);
447    }
448
449    #[test]
450    fn test_server_tool_use_deserializes() {
451        let json = json!({
452            "type": "server_tool_use",
453            "id": "srvtu_1",
454            "name": "web_search",
455            "input": {"query": "rust serde"}
456        });
457
458        let block: ContentBlock = serde_json::from_value(json).unwrap();
459        assert!(!block.is_unknown());
460        assert_eq!(block.block_type(), "server_tool_use");
461        if let ContentBlock::ServerToolUse(b) = &block {
462            assert_eq!(b.id, "srvtu_1");
463            assert_eq!(b.name, "web_search");
464            assert_eq!(b.input["query"], "rust serde");
465        } else {
466            panic!("Expected ServerToolUse variant");
467        }
468    }
469
470    #[test]
471    fn test_web_search_tool_result_deserializes() {
472        let json = json!({
473            "type": "web_search_tool_result",
474            "tool_use_id": "srvtu_1",
475            "content": [{"type": "web_search_result", "url": "https://example.com"}]
476        });
477
478        let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
479        assert_eq!(block.block_type(), "web_search_tool_result");
480        if let ContentBlock::WebSearchToolResult(b) = &block {
481            assert_eq!(b.tool_use_id, "srvtu_1");
482        } else {
483            panic!("Expected WebSearchToolResult variant");
484        }
485        // roundtrip
486        let reserialized = serde_json::to_value(&block).unwrap();
487        assert_eq!(json, reserialized);
488    }
489
490    #[test]
491    fn test_code_execution_tool_result_deserializes() {
492        let json = json!({
493            "type": "code_execution_tool_result",
494            "tool_use_id": "exec_1",
495            "content": {"stdout": "hello", "exit_code": 0}
496        });
497
498        let block: ContentBlock = serde_json::from_value(json).unwrap();
499        assert_eq!(block.block_type(), "code_execution_tool_result");
500        assert!(matches!(block, ContentBlock::CodeExecutionToolResult(_)));
501    }
502
503    #[test]
504    fn test_mcp_tool_use_deserializes() {
505        let json = json!({
506            "type": "mcp_tool_use",
507            "id": "mcp_tu_1",
508            "name": "custom_tool",
509            "server_name": "my-mcp-server",
510            "input": {"arg": "value"}
511        });
512
513        let block: ContentBlock = serde_json::from_value(json).unwrap();
514        assert_eq!(block.block_type(), "mcp_tool_use");
515        if let ContentBlock::McpToolUse(b) = &block {
516            assert_eq!(b.id, "mcp_tu_1");
517            assert_eq!(b.name, "custom_tool");
518            assert_eq!(b.server_name.as_deref(), Some("my-mcp-server"));
519        } else {
520            panic!("Expected McpToolUse variant");
521        }
522    }
523
524    #[test]
525    fn test_mcp_tool_result_deserializes() {
526        let json = json!({
527            "type": "mcp_tool_result",
528            "tool_use_id": "mcp_tu_1",
529            "content": "tool output text",
530            "is_error": false
531        });
532
533        let block: ContentBlock = serde_json::from_value(json).unwrap();
534        assert_eq!(block.block_type(), "mcp_tool_result");
535        if let ContentBlock::McpToolResult(b) = &block {
536            assert_eq!(b.tool_use_id, "mcp_tu_1");
537            assert_eq!(b.is_error, Some(false));
538        } else {
539            panic!("Expected McpToolResult variant");
540        }
541    }
542
543    #[test]
544    fn test_container_upload_deserializes() {
545        let json = json!({
546            "type": "container_upload",
547            "file_name": "output.csv",
548            "url": "https://storage.example.com/file"
549        });
550
551        let block: ContentBlock = serde_json::from_value(json).unwrap();
552        assert_eq!(block.block_type(), "container_upload");
553        assert!(matches!(block, ContentBlock::ContainerUpload(_)));
554    }
555
556    #[test]
557    fn test_known_blocks_still_work() {
558        let text_json = json!({"type": "text", "text": "hello"});
559        let block: ContentBlock = serde_json::from_value(text_json).unwrap();
560        assert!(!block.is_unknown());
561        assert_eq!(block.block_type(), "text");
562        assert!(matches!(block, ContentBlock::Text(TextBlock { text, .. }) if text == "hello"));
563
564        let tool_json =
565            json!({"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}});
566        let block: ContentBlock = serde_json::from_value(tool_json).unwrap();
567        assert_eq!(block.block_type(), "tool_use");
568        assert!(matches!(block, ContentBlock::ToolUse(_)));
569    }
570
571    #[test]
572    fn test_known_blocks_roundtrip() {
573        let text_json = json!({"type": "text", "text": "hello world"});
574        let block: ContentBlock = serde_json::from_value(text_json.clone()).unwrap();
575        let reserialized = serde_json::to_value(&block).unwrap();
576        assert_eq!(text_json, reserialized);
577    }
578
579    #[test]
580    fn test_assistant_message_with_server_tool_use() {
581        let json = r#"{
582            "type": "assistant",
583            "message": {
584                "id": "msg_1",
585                "role": "assistant",
586                "model": "claude-3",
587                "content": [
588                    {"type": "text", "text": "Let me search for that."},
589                    {"type": "server_tool_use", "id": "srvtu_1", "name": "web_search", "input": {"query": "test"}},
590                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}}
591                ]
592            },
593            "session_id": "abc"
594        }"#;
595
596        let output: crate::io::ClaudeOutput = serde_json::from_str(json).unwrap();
597        assert!(output.is_assistant_message());
598        let assistant = output.as_assistant().unwrap();
599        assert_eq!(assistant.message.content.len(), 3);
600        assert!(matches!(
601            &assistant.message.content[0],
602            ContentBlock::Text(_)
603        ));
604        assert!(matches!(
605            &assistant.message.content[1],
606            ContentBlock::ServerToolUse(_)
607        ));
608        assert!(matches!(
609            &assistant.message.content[2],
610            ContentBlock::ToolUse(_)
611        ));
612
613        // text_content() still works, skipping non-text blocks
614        assert_eq!(
615            output.text_content(),
616            Some("Let me search for that.".to_string())
617        );
618        // tool_uses() only returns regular tool_use
619        assert_eq!(output.tool_uses().count(), 1);
620    }
621
622    #[test]
623    fn test_text_block_with_citations() {
624        let json = json!({
625            "type": "text",
626            "text": "According to the documentation...",
627            "citations": [
628                {"type": "web_search_result_location", "url": "https://example.com", "title": "Example"}
629            ]
630        });
631
632        let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
633        if let ContentBlock::Text(t) = &block {
634            assert_eq!(t.text, "According to the documentation...");
635            assert_eq!(t.citations.len(), 1);
636            assert_eq!(t.citations[0]["url"], "https://example.com");
637        } else {
638            panic!("Expected Text variant");
639        }
640        // roundtrip preserves citations
641        let reserialized = serde_json::to_value(&block).unwrap();
642        assert_eq!(json, reserialized);
643    }
644
645    #[test]
646    fn test_text_block_without_citations_defaults_empty() {
647        let json = json!({"type": "text", "text": "no citations"});
648        let block: ContentBlock = serde_json::from_value(json).unwrap();
649        if let ContentBlock::Text(t) = &block {
650            assert!(t.citations.is_empty());
651        } else {
652            panic!("Expected Text variant");
653        }
654        // serialization omits empty citations
655        let reserialized = serde_json::to_value(&block).unwrap();
656        assert!(reserialized.get("citations").is_none());
657    }
658
659    #[test]
660    fn test_missing_type_field_errors() {
661        let json = json!({"text": "no type field"});
662        let result = serde_json::from_value::<ContentBlock>(json);
663        assert!(result.is_err());
664    }
665}