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}