claude_session_types/events/message.rs
1//! Message content types (Level 2)
2//!
3//! Found in `.message.content[]` for both user and assistant events.
4//!
5//! # Content Block Types
6//!
7//! ```text
8//! Content Blocks (.message.content[].type)
9//! ├── text (18,557) - Text content from user/assistant
10//! ├── tool_use (30,782) - Tool invocations in assistant messages
11//! ├── tool_result (29,848) - Tool results in user messages
12//! ├── image (5) - Image attachments (base64)
13//! └── attachment - File attachments, hooks, reminders
14//! ```
15//!
16//! # Usage
17//!
18//! Content blocks appear in:
19//! - User messages: `text`, `tool_result`, `image`
20//! - Assistant messages: `text`, `tool_use`
21//! - Progress normalized messages: all types
22
23use serde::{Deserialize, Serialize};
24use serde_json::Value as JsonValue;
25
26/// Message content wrapper
27///
28/// Contains role and content blocks for both user and assistant messages.
29///
30/// # Example
31///
32/// ```json
33/// {
34/// "role": "user",
35/// "content": [
36/// {"type": "text", "text": "Hello"},
37/// {"type": "tool_result", "tool_use_id": "...", "content": "..."}
38/// ]
39/// }
40/// ```
41///
42/// # Wire format drift
43///
44/// Real Claude Code transcripts (v2.1.2xx) put a bare JSON **string** in
45/// `content` for plain single-turn human prompts, not the documented array of
46/// content blocks — the array form is only used once the turn carries
47/// structured pieces (tool results, images, thinking). Both shapes are
48/// accepted: a bare string normalizes to a single [`ContentBlock::Text`].
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct MessageContent {
51 /// Role: "user" or "assistant"
52 pub role: String,
53
54 /// Content blocks (text, tool use, tool results, etc.)
55 ///
56 /// Accepts either the documented array-of-blocks shape or a bare string
57 /// (the common shape for plain-text human prompts on disk).
58 #[serde(deserialize_with = "deserialize_content_blocks")]
59 pub content: Vec<ContentBlock>,
60}
61
62/// Deserialize `content` as either a bare string or an array of content blocks.
63///
64/// A bare string becomes a single `ContentBlock::Text`, matching how the array
65/// form represents the same plain-text turn.
66fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
67where
68 D: serde::Deserializer<'de>,
69{
70 #[derive(Deserialize)]
71 #[serde(untagged)]
72 enum ContentRepr {
73 Text(String),
74 Blocks(Vec<ContentBlock>),
75 }
76
77 match ContentRepr::deserialize(deserializer)? {
78 ContentRepr::Text(text) => Ok(vec![ContentBlock::Text(TextBlock { text })]),
79 ContentRepr::Blocks(blocks) => Ok(blocks),
80 }
81}
82
83/// Content block discriminator
84///
85/// All possible content block types found in message content arrays.
86/// Uses serde's tagged enum to automatically parse based on `type` field.
87///
88/// # Links
89///
90/// - User messages contain: `Text`, `ToolResult`, `Image`
91/// - Assistant messages contain: `Text`, `ToolUse`, `Thinking` (when extended thinking enabled)
92/// - Progress normalized messages contain: all types including `Attachment`
93///
94/// # Frequency (per large session)
95///
96/// - `ToolUse`: ~30k occurrences
97/// - `ToolResult`: ~30k occurrences
98/// - `Text`: ~19k occurrences
99/// - `Thinking`: Variable (only when extended thinking mode enabled)
100/// - `Image`: ~5 occurrences
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(tag = "type", rename_all = "snake_case")]
103pub enum ContentBlock {
104 /// Text content block
105 ///
106 /// Found in: user messages, assistant messages
107 ///
108 /// Contains plain text or markdown from user input or assistant responses.
109 Text(TextBlock),
110
111 /// Tool use invocation
112 ///
113 /// Found in: assistant messages
114 ///
115 /// Represents Claude invoking a tool (Read, Write, Edit, Bash, etc.)
116 ToolUse(ToolUseBlock),
117
118 /// Tool execution result
119 ///
120 /// Found in: user messages (as tool result feedback)
121 ///
122 /// Contains the output from a tool execution, sent back to Claude.
123 ToolResult(ToolResultBlock),
124
125 /// Image attachment
126 ///
127 /// Found in: user messages
128 ///
129 /// Base64-encoded image data sent by user.
130 Image(ImageBlock),
131
132 /// Attachment (files, hooks, reminders)
133 ///
134 /// Found in: progress normalized messages
135 ///
136 /// See `attachment` module for detailed attachment types.
137 #[serde(rename = "attachment")]
138 Attachment(crate::events::attachment::AttachmentBlock),
139
140 /// Thinking block (extended thinking mode)
141 ///
142 /// Found in: assistant messages (only when user enables extended thinking)
143 ///
144 /// Contains Claude's reasoning process with signature verification.
145 /// This is OPTIONAL - only present when extended thinking mode is enabled.
146 Thinking(ThinkingBlock),
147
148 /// Unknown content block type (forward compatibility)
149 #[serde(other)]
150 Unknown,
151}
152
153impl ContentBlock {
154 /// Extract text if this is a text block
155 #[must_use]
156 pub fn as_text(&self) -> Option<&str> {
157 match self {
158 Self::Text(text) => Some(&text.text),
159 _ => None,
160 }
161 }
162
163 /// Extract tool use if this is a tool use block
164 #[must_use]
165 pub fn as_tool_use(&self) -> Option<(&str, &str, &JsonValue)> {
166 match self {
167 Self::ToolUse(tool) => Some((&tool.id, &tool.name, &tool.input)),
168 _ => None,
169 }
170 }
171
172 /// Extract thinking if this is a thinking block
173 #[must_use]
174 pub fn as_thinking(&self) -> Option<&str> {
175 match self {
176 Self::Thinking(thinking) => Some(&thinking.thinking),
177 _ => None,
178 }
179 }
180
181 /// Check if this is a tool result
182 #[must_use]
183 pub fn is_tool_result(&self) -> bool {
184 matches!(self, Self::ToolResult(_))
185 }
186
187 /// Check if this is an attachment
188 #[must_use]
189 pub fn is_attachment(&self) -> bool {
190 matches!(self, Self::Attachment(_))
191 }
192
193 /// Check if this is a thinking block
194 #[must_use]
195 pub fn is_thinking(&self) -> bool {
196 matches!(self, Self::Thinking(_))
197 }
198}
199
200/// Text content block
201///
202/// Contains plain text or markdown from user input or assistant responses.
203///
204/// # Example
205///
206/// ```json
207/// {
208/// "type": "text",
209/// "text": "I'll help you implement that feature."
210/// }
211/// ```
212///
213/// # Usage
214///
215/// - User prompts
216/// - Assistant explanations
217/// - Agent task descriptions
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct TextBlock {
220 /// Text content (plain text or markdown)
221 pub text: String,
222}
223
224/// Tool use invocation block
225///
226/// Represents Claude invoking a tool (Read, Write, Edit, Bash, etc.)
227///
228/// # Example
229///
230/// ```json
231/// {
232/// "type": "tool_use",
233/// "id": "toolu_abc123",
234/// "name": "Read",
235/// "input": {
236/// "file_path": "/path/to/file.rs"
237/// }
238/// }
239/// ```
240///
241/// # Links
242///
243/// - `id` links to `ToolResultBlock.tool_use_id` in user messages
244/// - `id` links to `ProgressEvent.tool_use_id` for progress updates
245///
246/// # Common Tool Names
247///
248/// - `Read` - Read file contents
249/// - `Write` - Write new file
250/// - `Edit` - Edit existing file
251/// - `Bash` - Execute bash command
252/// - `Glob` - Search for files by pattern
253/// - `Grep` - Search file contents
254/// - `TodoWrite` - Update todo list
255/// - `Agent` - Spawn sub-agent
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct ToolUseBlock {
258 /// Unique tool use ID
259 ///
260 /// Links to tool result in user message
261 /// Format: `"toolu_{random}"` or `"{tool_name}-{uuid}"`
262 pub id: String,
263
264 /// Tool name
265 ///
266 /// Common tools: Read, Write, Edit, Bash, Glob, Grep, Agent
267 pub name: String,
268
269 /// Tool input parameters (JSON object)
270 ///
271 /// Structure depends on tool type:
272 /// - Read: `{"file_path": "/path"}`
273 /// - Write: `{"file_path": "/path", "content": "..."}`
274 /// - Edit: `{"file_path": "/path", "old_string": "...", "new_string": "..."}`
275 /// - Bash: `{"command": "cargo build", "description": "..."}`
276 pub input: JsonValue,
277}
278
279/// Tool execution result block
280///
281/// Contains the output from a tool execution, sent back to Claude.
282///
283/// # Example
284///
285/// ```json
286/// {
287/// "type": "tool_result",
288/// "tool_use_id": "toolu_abc123",
289/// "content": {
290/// "type": "text",
291/// "content": "File contents here..."
292/// }
293/// }
294/// ```
295///
296/// # Links
297///
298/// - `tool_use_id` links back to `ToolUseBlock.id` in assistant message
299/// - Nested `content` can contain `ToolUseResult` for file operations
300///
301/// # Content Types
302///
303/// Content can be:
304/// - String: simple text output
305/// - Object with `type` field: structured result (see `tool_result` module)
306/// - Array: multiple result items
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct ToolResultBlock {
309 /// Links back to tool use that generated this result
310 #[serde(rename = "tool_use_id")]
311 pub tool_use_id: String,
312
313 /// Tool execution result (JSON value)
314 ///
315 /// Can be:
316 /// - String: simple output (`"cargo build completed"`)
317 /// - Object: structured result with `type` field (see `ToolUseResult`)
318 /// - Array: multiple result items
319 pub content: JsonValue,
320
321 /// Structured tool use result (for file operations)
322 ///
323 /// Present when tool modifies files (Write, Edit). Deserialized leniently:
324 /// a `type` tag that no known `ToolUseResult` shape matches (e.g. a
325 /// `"text"`-tagged result missing the plain `content` field) becomes
326 /// `None` instead of failing the whole enclosing event — see
327 /// [`crate::events::tool_result::deserialize_tool_use_result_lenient`].
328 #[serde(
329 rename = "toolUseResult",
330 deserialize_with = "crate::events::tool_result::deserialize_tool_use_result_lenient",
331 default
332 )]
333 pub tool_use_result: Option<crate::events::tool_result::ToolUseResult>,
334}
335
336/// Thinking block (extended thinking mode)
337///
338/// Contains Claude's reasoning process when extended thinking mode is enabled.
339/// This block is OPTIONAL and only appears when the user explicitly enables
340/// extended thinking in their Claude settings.
341///
342/// # Example
343///
344/// ```json
345/// {
346/// "type": "thinking",
347/// "thinking": "The user is asking about...",
348/// "signature": "ErQYCkYICxgCKkA1FuCoAqSF..."
349/// }
350/// ```
351///
352/// # Usage
353///
354/// - Available when extended thinking mode is enabled
355/// - Contains full reasoning process
356/// - Includes cryptographic signature for verification
357/// - Makes Claude competitive with Gemini for reasoning extraction
358///
359/// # Frequency
360///
361/// Variable - only present when extended thinking is enabled by user
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ThinkingBlock {
364 /// Full reasoning text
365 ///
366 /// Contains Claude's complete thought process, including:
367 /// - Analysis of the request
368 /// - Consideration of alternatives
369 /// - Decision-making rationale
370 /// - Context evaluation
371 pub thinking: String,
372
373 /// Cryptographic signature for verification
374 ///
375 /// Used to verify the authenticity of the thinking content.
376 /// Optional field that may not be present in all thinking blocks.
377 pub signature: Option<String>,
378}
379
380/// Image attachment block
381///
382/// Base64-encoded image data sent by user.
383///
384/// # Example
385///
386/// ```json
387/// {
388/// "type": "image",
389/// "source": {
390/// "type": "base64",
391/// "media_type": "image/png",
392/// "data": "iVBORw0KGgoAAAANSUhEUgAA..."
393/// }
394/// }
395/// ```
396///
397/// # Usage
398///
399/// - User sends screenshot
400/// - User sends diagram for analysis
401/// - User sends error message screenshot
402///
403/// # Frequency
404///
405/// Very rare in typical sessions (~5 per large session)
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ImageBlock {
408 /// Image source (base64 data)
409 pub source: ImageSource,
410}
411
412/// Image source (base64 encoded)
413///
414/// Contains base64-encoded image data and media type.
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ImageSource {
417 /// Source type (always "base64")
418 #[serde(rename = "type")]
419 pub source_type: String,
420
421 /// Media type (MIME type)
422 ///
423 /// Examples: `"image/png"`, `"image/jpeg"`, `"image/webp"`
424 pub media_type: String,
425
426 /// Base64-encoded image data
427 pub data: String,
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_parse_text_block() {
436 let json = r#"{
437 "type": "text",
438 "text": "Hello world"
439 }"#;
440
441 let block: ContentBlock = serde_json::from_str(json).unwrap();
442 assert!(matches!(block, ContentBlock::Text(_)));
443
444 if let ContentBlock::Text(text) = block {
445 assert_eq!(text.text, "Hello world");
446 }
447 }
448
449 #[test]
450 fn test_parse_tool_use_block() {
451 let json = r#"{
452 "type": "tool_use",
453 "id": "toolu_abc123",
454 "name": "Read",
455 "input": {
456 "file_path": "/test/file.rs"
457 }
458 }"#;
459
460 let block: ContentBlock = serde_json::from_str(json).unwrap();
461 assert!(matches!(block, ContentBlock::ToolUse(_)));
462
463 if let ContentBlock::ToolUse(tool) = block {
464 assert_eq!(tool.id, "toolu_abc123");
465 assert_eq!(tool.name, "Read");
466 assert_eq!(tool.input["file_path"], "/test/file.rs");
467 }
468 }
469
470 #[test]
471 fn test_parse_tool_result_block() {
472 let json = r#"{
473 "type": "tool_result",
474 "tool_use_id": "toolu_abc123",
475 "content": "File contents here"
476 }"#;
477
478 let block: ContentBlock = serde_json::from_str(json).unwrap();
479 assert!(matches!(block, ContentBlock::ToolResult(_)));
480 assert!(block.is_tool_result());
481
482 if let ContentBlock::ToolResult(result) = block {
483 assert_eq!(result.tool_use_id, "toolu_abc123");
484 assert_eq!(result.content, "File contents here");
485 }
486 }
487
488 #[test]
489 fn test_parse_image_block() {
490 let json = r#"{
491 "type": "image",
492 "source": {
493 "type": "base64",
494 "media_type": "image/png",
495 "data": "iVBORw0KGgo="
496 }
497 }"#;
498
499 let block: ContentBlock = serde_json::from_str(json).unwrap();
500 assert!(matches!(block, ContentBlock::Image(_)));
501
502 if let ContentBlock::Image(image) = block {
503 assert_eq!(image.source.source_type, "base64");
504 assert_eq!(image.source.media_type, "image/png");
505 assert_eq!(image.source.data, "iVBORw0KGgo=");
506 }
507 }
508
509 #[test]
510 fn test_content_block_helpers() {
511 let text_block = ContentBlock::Text(TextBlock {
512 text: "Test".to_string(),
513 });
514 assert_eq!(text_block.as_text(), Some("Test"));
515 assert!(!text_block.is_tool_result());
516
517 let tool_block = ContentBlock::ToolUse(ToolUseBlock {
518 id: "tool-1".to_string(),
519 name: "Read".to_string(),
520 input: serde_json::json!({}),
521 });
522 let tool_use = tool_block.as_tool_use();
523 assert!(tool_use.is_some());
524 let (id, name, _) = tool_use.unwrap();
525 assert_eq!(id, "tool-1");
526 assert_eq!(name, "Read");
527 }
528
529 #[test]
530 fn test_parse_thinking_block() {
531 let json = r#"{
532 "type": "thinking",
533 "thinking": "Let me analyze this request carefully...",
534 "signature": "ErQYCkYICxgCKkA1FuCoAqSF..."
535 }"#;
536
537 let block: ContentBlock = serde_json::from_str(json).unwrap();
538 assert!(matches!(block, ContentBlock::Thinking(_)));
539 assert!(block.is_thinking());
540
541 if let ContentBlock::Thinking(thinking) = block {
542 assert_eq!(thinking.thinking, "Let me analyze this request carefully...");
543 assert!(thinking.signature.is_some());
544 assert_eq!(thinking.signature.unwrap(), "ErQYCkYICxgCKkA1FuCoAqSF...");
545 }
546 }
547
548 #[test]
549 fn test_parse_thinking_block_without_signature() {
550 let json = r#"{
551 "type": "thinking",
552 "thinking": "Analyzing the problem..."
553 }"#;
554
555 let block: ContentBlock = serde_json::from_str(json).unwrap();
556 assert!(matches!(block, ContentBlock::Thinking(_)));
557
558 if let ContentBlock::Thinking(thinking) = block {
559 assert_eq!(thinking.thinking, "Analyzing the problem...");
560 assert!(thinking.signature.is_none());
561 }
562 }
563
564 #[test]
565 fn test_content_block_as_thinking() {
566 let thinking_block = ContentBlock::Thinking(ThinkingBlock {
567 thinking: "Test reasoning".to_string(),
568 signature: Some("sig123".to_string()),
569 });
570
571 assert_eq!(thinking_block.as_thinking(), Some("Test reasoning"));
572 assert!(thinking_block.is_thinking());
573
574 let text_block = ContentBlock::Text(TextBlock {
575 text: "Test".to_string(),
576 });
577 assert_eq!(text_block.as_thinking(), None);
578 assert!(!text_block.is_thinking());
579 }
580
581 #[test]
582 fn test_message_content() {
583 let json = r#"{
584 "role": "assistant",
585 "content": [
586 {
587 "type": "text",
588 "text": "Let me read the file"
589 },
590 {
591 "type": "tool_use",
592 "id": "tool-123",
593 "name": "Read",
594 "input": {"file_path": "/test.rs"}
595 }
596 ]
597 }"#;
598
599 let message: MessageContent = serde_json::from_str(json).unwrap();
600 assert_eq!(message.role, "assistant");
601 assert_eq!(message.content.len(), 2);
602
603 assert!(matches!(message.content[0], ContentBlock::Text(_)));
604 assert!(matches!(message.content[1], ContentBlock::ToolUse(_)));
605 }
606
607 #[test]
608 fn test_message_with_thinking() {
609 let json = r#"{
610 "role": "assistant",
611 "content": [
612 {
613 "type": "thinking",
614 "thinking": "I need to analyze this carefully..."
615 },
616 {
617 "type": "text",
618 "text": "Based on my analysis..."
619 }
620 ]
621 }"#;
622
623 let message: MessageContent = serde_json::from_str(json).unwrap();
624 assert_eq!(message.role, "assistant");
625 assert_eq!(message.content.len(), 2);
626
627 assert!(matches!(message.content[0], ContentBlock::Thinking(_)));
628 assert!(matches!(message.content[1], ContentBlock::Text(_)));
629
630 // Verify we can extract thinking
631 assert!(message.content[0].is_thinking());
632 assert_eq!(
633 message.content[0].as_thinking(),
634 Some("I need to analyze this carefully...")
635 );
636 }
637}