Skip to main content

apiari_claude_sdk/
types.rs

1//! All message types from the Claude CLI stream-json protocol.
2//!
3//! When the CLI is invoked with `--output-format stream-json --verbose`, every
4//! line on stdout is a JSON object whose `"type"` field determines the variant.
5//!
6//! The top-level message types are:
7//!
8//! | `type`               | Rust type            | Description                                       |
9//! |----------------------|----------------------|---------------------------------------------------|
10//! | `system`             | [`SystemMessage`]    | Metadata emitted at session start.                |
11//! | `user`               | [`UserMessage`]      | Echo of the user turn.                            |
12//! | `assistant`          | [`AssistantMessage`] | Model turn with text / tool-use content.          |
13//! | `result`             | [`ResultMessage`]    | Final summary (cost, duration, session ID).       |
14//! | `stream_event`       | [`StreamEvent`]      | Raw Anthropic API streaming event (partial data). |
15//! | `rate_limit_event`   | [`RateLimitEvent`]   | Rate limit status information.                    |
16
17use serde::{Deserialize, Serialize};
18
19// ---------------------------------------------------------------------------
20// Top-level message envelope
21// ---------------------------------------------------------------------------
22
23/// A single NDJSON message read from the Claude CLI stdout.
24///
25/// Deserialized via `#[serde(tag = "type")]` so the `"type"` field selects
26/// the variant.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "type", rename_all = "snake_case")]
29pub enum Message {
30    /// Session metadata emitted once at the start.
31    System(SystemMessage),
32    /// Echo of a user turn (our input reflected back).
33    User(UserMessage),
34    /// A model response turn.
35    Assistant(AssistantMessage),
36    /// Final summary after the conversation completes.
37    Result(ResultMessage),
38    /// A raw API streaming event (only when `--include-partial-messages`).
39    StreamEvent(StreamEvent),
40    /// Rate limit status information.
41    RateLimitEvent(RateLimitEvent),
42}
43
44// ---------------------------------------------------------------------------
45// System message
46// ---------------------------------------------------------------------------
47
48/// Metadata emitted once when the session starts.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SystemMessage {
51    /// Sub-classification of the system message (e.g. `"init"`).
52    pub subtype: String,
53
54    /// The full raw JSON data for forward-compatibility.
55    #[serde(flatten)]
56    pub data: serde_json::Map<String, serde_json::Value>,
57}
58
59// ---------------------------------------------------------------------------
60// User message
61// ---------------------------------------------------------------------------
62
63/// A user turn, either from our stdin input or echoed back by the CLI.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct UserMessage {
66    /// The message content (text string or structured content blocks).
67    pub message: UserMessageContent,
68
69    /// Session-unique identifier for this message.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub uuid: Option<String>,
72
73    /// If this user message is a tool result, the parent tool-use ID.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub parent_tool_use_id: Option<String>,
76
77    /// When the user message carries a tool result.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub tool_use_result: Option<serde_json::Value>,
80}
81
82/// The inner content of a user message.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct UserMessageContent {
85    /// Always `"user"`.
86    #[serde(default = "default_user_role")]
87    pub role: String,
88
89    /// Either a bare string or a vec of content blocks.
90    pub content: UserContent,
91}
92
93fn default_user_role() -> String {
94    "user".to_owned()
95}
96
97/// User content: either a simple text string or structured blocks.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(untagged)]
100pub enum UserContent {
101    /// A plain text string.
102    Text(String),
103    /// An array of content blocks.
104    Blocks(Vec<ContentBlock>),
105}
106
107// ---------------------------------------------------------------------------
108// Assistant message
109// ---------------------------------------------------------------------------
110
111/// The top-level assistant message envelope as emitted by the CLI.
112///
113/// The actual model content is nested inside the `message` field, with
114/// metadata like `session_id` and `parent_tool_use_id` at the envelope level.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AssistantMessage {
117    /// The inner message containing model output (content blocks, model name, etc.).
118    pub message: AssistantMessageContent,
119
120    /// If this turn was produced inside a tool-use (sub-agent), the parent ID.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub parent_tool_use_id: Option<String>,
123
124    /// The session ID for this conversation.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub session_id: Option<String>,
127
128    /// Session-unique identifier for this message.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub uuid: Option<String>,
131}
132
133/// The inner content of an assistant message, containing the model's response.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct AssistantMessageContent {
136    /// The model that produced this turn (e.g. `"claude-opus-4-6"`).
137    pub model: String,
138
139    /// Content blocks: text, thinking, tool_use, or tool_result.
140    #[serde(default)]
141    pub content: Vec<ContentBlock>,
142
143    /// Anthropic message ID (e.g. `"msg_01NwW..."`).
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub id: Option<String>,
146
147    /// The role, always `"assistant"`.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub role: Option<String>,
150
151    /// Why the model stopped generating (e.g. `"end_turn"`, `"tool_use"`).
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub stop_reason: Option<String>,
154
155    /// Token usage for this turn.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub usage: Option<serde_json::Value>,
158
159    /// Forward-compatibility: capture any extra fields we don't know about.
160    #[serde(flatten)]
161    pub extra: serde_json::Map<String, serde_json::Value>,
162}
163
164// ---------------------------------------------------------------------------
165// Result message
166// ---------------------------------------------------------------------------
167
168/// Final summary emitted when the conversation finishes.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ResultMessage {
171    /// Sub-classification (e.g. `"success"`, `"error"`, `"max_turns"`).
172    pub subtype: String,
173
174    /// Wall-clock duration in milliseconds.
175    pub duration_ms: u64,
176
177    /// API-only duration in milliseconds.
178    pub duration_api_ms: u64,
179
180    /// Whether the conversation ended in an error state.
181    pub is_error: bool,
182
183    /// How many agentic turns were executed.
184    pub num_turns: u64,
185
186    /// The session ID (useful for `--resume`).
187    pub session_id: String,
188
189    /// Total estimated cost in USD, if available.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub total_cost_usd: Option<f64>,
192
193    /// Token usage breakdown.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub usage: Option<serde_json::Value>,
196
197    /// The final text result, if any.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub result: Option<String>,
200
201    /// Structured output when `--json-schema` was provided.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub structured_output: Option<serde_json::Value>,
204}
205
206// ---------------------------------------------------------------------------
207// Stream event (partial messages)
208// ---------------------------------------------------------------------------
209
210/// A raw Anthropic API streaming event, emitted when
211/// `--include-partial-messages` is set.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct StreamEvent {
214    /// Session-unique identifier.
215    pub uuid: String,
216
217    /// The session ID.
218    pub session_id: String,
219
220    /// The raw API event payload.
221    pub event: StreamEventPayload,
222
223    /// Parent tool-use ID if this event is from a sub-agent.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub parent_tool_use_id: Option<String>,
226}
227
228// ---------------------------------------------------------------------------
229// Rate limit event
230// ---------------------------------------------------------------------------
231
232/// Rate limit status information emitted by the CLI.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct RateLimitEvent {
235    /// Rate limit status details.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub rate_limit_info: Option<serde_json::Value>,
238
239    /// Session-unique identifier.
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub uuid: Option<String>,
242
243    /// The session ID.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub session_id: Option<String>,
246}
247
248/// The inner event payload from the Anthropic streaming API.
249///
250/// This mirrors the Server-Sent Events that the Anthropic API emits.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252#[serde(tag = "type", rename_all = "snake_case")]
253pub enum StreamEventPayload {
254    /// Signals the start of a new message, includes message metadata.
255    MessageStart {
256        /// The message object with `id`, `role`, `model`, etc.
257        message: serde_json::Value,
258    },
259
260    /// Signals the start of a content block at the given index.
261    ContentBlockStart {
262        /// Zero-based index of this content block.
263        index: u64,
264        /// The initial content block (type, and any initial data).
265        content_block: ContentBlockInfo,
266    },
267
268    /// An incremental update to the content block at the given index.
269    ContentBlockDelta {
270        /// Zero-based index of the content block being updated.
271        index: u64,
272        /// The delta payload.
273        delta: Delta,
274    },
275
276    /// Signals that the content block at the given index is complete.
277    ContentBlockStop {
278        /// Zero-based index of the completed content block.
279        index: u64,
280    },
281
282    /// An update to the top-level message (e.g. `stop_reason`, usage).
283    MessageDelta {
284        /// The delta payload.
285        delta: serde_json::Value,
286        /// Updated usage statistics.
287        #[serde(default, skip_serializing_if = "Option::is_none")]
288        usage: Option<serde_json::Value>,
289    },
290
291    /// Signals that the entire message is complete.
292    MessageStop,
293
294    /// Catch-all for forward-compatibility with unknown event types.
295    #[serde(other)]
296    Unknown,
297}
298
299/// Information about a content block at its start.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[serde(tag = "type", rename_all = "snake_case")]
302pub enum ContentBlockInfo {
303    /// A text content block.
304    Text {
305        /// Initial text (usually empty at start).
306        #[serde(default)]
307        text: String,
308    },
309    /// A thinking content block.
310    Thinking {
311        /// Initial thinking text.
312        #[serde(default)]
313        thinking: String,
314    },
315    /// A tool-use content block.
316    ToolUse {
317        /// The tool-use ID.
318        id: String,
319        /// The tool name.
320        name: String,
321        /// Initial input (usually empty object or partial JSON).
322        #[serde(default)]
323        input: serde_json::Value,
324    },
325}
326
327/// An incremental delta within a `content_block_delta` event.
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(tag = "type", rename_all = "snake_case")]
330pub enum Delta {
331    /// Incremental text.
332    TextDelta {
333        /// The text fragment.
334        text: String,
335    },
336    /// Incremental thinking text.
337    ThinkingDelta {
338        /// The thinking fragment.
339        thinking: String,
340    },
341    /// Incremental JSON for a tool-use input.
342    InputJsonDelta {
343        /// Partial JSON string to append.
344        partial_json: String,
345    },
346}
347
348// ---------------------------------------------------------------------------
349// Content blocks (used in assistant & user messages)
350// ---------------------------------------------------------------------------
351
352/// A content block inside an assistant or user message.
353#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(tag = "type", rename_all = "snake_case")]
355pub enum ContentBlock {
356    /// Plain text content.
357    Text {
358        /// The text content.
359        text: String,
360    },
361    /// Model thinking / chain-of-thought (extended thinking).
362    Thinking {
363        /// The thinking text.
364        thinking: String,
365        /// Cryptographic signature for verification.
366        signature: String,
367    },
368    /// A request for the SDK to execute a tool.
369    ToolUse {
370        /// Unique identifier for this tool invocation.
371        id: String,
372        /// The tool name (e.g. `"Bash"`, `"Edit"`, `"Read"`).
373        name: String,
374        /// The tool input parameters.
375        input: serde_json::Value,
376    },
377    /// The result of a tool execution.
378    ToolResult {
379        /// The `id` from the corresponding `ToolUse` block.
380        tool_use_id: String,
381        /// The result content (text string, structured blocks, or null).
382        #[serde(default, skip_serializing_if = "Option::is_none")]
383        content: Option<serde_json::Value>,
384        /// Whether the tool execution failed.
385        #[serde(default, skip_serializing_if = "Option::is_none")]
386        is_error: Option<bool>,
387    },
388}
389
390// ---------------------------------------------------------------------------
391// Input messages (sent to stdin with --input-format stream-json)
392// ---------------------------------------------------------------------------
393
394/// A message we write to the CLI's stdin when using `--input-format stream-json`.
395///
396/// The CLI expects a JSON object per line. For user messages, the format is:
397/// ```json
398/// {"type":"user","message":{"role":"user","content":"Hello"}}
399/// ```
400#[derive(Debug, Clone, Serialize, Deserialize)]
401#[serde(tag = "type", rename_all = "snake_case")]
402pub enum InputMessage {
403    /// A user message to send to the model.
404    User {
405        /// The message payload.
406        message: InputMessageContent,
407    },
408}
409
410/// The inner content of an input message sent to stdin.
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct InputMessageContent {
413    /// Always `"user"`.
414    pub role: String,
415    /// The content: text or content blocks.
416    pub content: InputContent,
417}
418
419/// Content for an input message.
420#[derive(Debug, Clone, Serialize, Deserialize)]
421#[serde(untagged)]
422pub enum InputContent {
423    /// A plain text string.
424    Text(String),
425    /// Structured content blocks (for tool results, images, etc.).
426    Blocks(Vec<InputContentBlock>),
427}
428
429/// A content block within an input message.
430#[derive(Debug, Clone, Serialize, Deserialize)]
431#[serde(tag = "type", rename_all = "snake_case")]
432pub enum InputContentBlock {
433    /// Plain text.
434    Text {
435        /// The text content.
436        text: String,
437    },
438    /// An image block (base64-encoded).
439    Image {
440        /// The image source.
441        source: ImageSource,
442    },
443    /// A tool result block.
444    ToolResult {
445        /// The tool_use ID this result corresponds to.
446        tool_use_id: String,
447        /// The result content.
448        #[serde(default, skip_serializing_if = "Option::is_none")]
449        content: Option<String>,
450        /// Whether the tool errored.
451        #[serde(default, skip_serializing_if = "Option::is_none")]
452        is_error: Option<bool>,
453    },
454}
455
456/// Source data for an image content block.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct ImageSource {
459    /// Always `"base64"`.
460    #[serde(rename = "type")]
461    pub source_type: String,
462    /// MIME type (e.g. `"image/png"`, `"image/jpeg"`).
463    pub media_type: String,
464    /// Base64-encoded image data.
465    pub data: String,
466}
467
468// ---------------------------------------------------------------------------
469// Helpers
470// ---------------------------------------------------------------------------
471
472impl InputMessage {
473    /// Create a simple text user message.
474    pub fn user_text(text: impl Into<String>) -> Self {
475        InputMessage::User {
476            message: InputMessageContent {
477                role: "user".to_owned(),
478                content: InputContent::Text(text.into()),
479            },
480        }
481    }
482
483    /// Create a user message with text and images.
484    ///
485    /// Images should be provided as `(media_type, base64_data)` tuples.
486    pub fn user_with_images(
487        text: impl Into<String>,
488        images: Vec<(String, String)>,
489    ) -> Self {
490        let mut blocks: Vec<InputContentBlock> = images
491            .into_iter()
492            .map(|(media_type, data)| InputContentBlock::Image {
493                source: ImageSource {
494                    source_type: "base64".to_owned(),
495                    media_type,
496                    data,
497                },
498            })
499            .collect();
500        let text = text.into();
501        if !text.is_empty() {
502            blocks.push(InputContentBlock::Text { text });
503        }
504        InputMessage::User {
505            message: InputMessageContent {
506                role: "user".to_owned(),
507                content: InputContent::Blocks(blocks),
508            },
509        }
510    }
511
512    /// Create a tool-result user message.
513    pub fn tool_result(
514        tool_use_id: impl Into<String>,
515        output: impl Into<String>,
516        is_error: bool,
517    ) -> Self {
518        InputMessage::User {
519            message: InputMessageContent {
520                role: "user".to_owned(),
521                content: InputContent::Blocks(vec![InputContentBlock::ToolResult {
522                    tool_use_id: tool_use_id.into(),
523                    content: Some(output.into()),
524                    is_error: if is_error { Some(true) } else { None },
525                }]),
526            },
527        }
528    }
529}
530
531impl Message {
532    /// Returns `true` if this is a [`ResultMessage`].
533    pub fn is_result(&self) -> bool {
534        matches!(self, Message::Result(_))
535    }
536
537    /// Returns `true` if this is a [`StreamEvent`].
538    pub fn is_stream_event(&self) -> bool {
539        matches!(self, Message::StreamEvent(_))
540    }
541
542    /// Returns `true` if this is an [`AssistantMessage`].
543    pub fn is_assistant(&self) -> bool {
544        matches!(self, Message::Assistant(_))
545    }
546
547    /// Try to extract a reference to the inner [`AssistantMessage`].
548    pub fn as_assistant(&self) -> Option<&AssistantMessage> {
549        match self {
550            Message::Assistant(m) => Some(m),
551            _ => None,
552        }
553    }
554
555    /// Try to extract a reference to the inner [`ResultMessage`].
556    pub fn as_result(&self) -> Option<&ResultMessage> {
557        match self {
558            Message::Result(m) => Some(m),
559            _ => None,
560        }
561    }
562
563    /// Try to extract a reference to the inner [`StreamEvent`].
564    pub fn as_stream_event(&self) -> Option<&StreamEvent> {
565        match self {
566            Message::StreamEvent(e) => Some(e),
567            _ => None,
568        }
569    }
570}