1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4use uuid::Uuid;
5
6use super::content_blocks::{deserialize_content_blocks, ContentBlock};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum SystemSubtype {
15 Init,
16 Status,
17 CompactBoundary,
18 ThinkingTokens,
19 TaskStarted,
20 TaskProgress,
21 TaskUpdated,
22 TaskNotification,
23 Unknown(String),
25}
26
27impl SystemSubtype {
28 pub fn as_str(&self) -> &str {
29 match self {
30 Self::Init => "init",
31 Self::Status => "status",
32 Self::CompactBoundary => "compact_boundary",
33 Self::ThinkingTokens => "thinking_tokens",
34 Self::TaskStarted => "task_started",
35 Self::TaskProgress => "task_progress",
36 Self::TaskUpdated => "task_updated",
37 Self::TaskNotification => "task_notification",
38 Self::Unknown(s) => s.as_str(),
39 }
40 }
41}
42
43impl fmt::Display for SystemSubtype {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str(self.as_str())
46 }
47}
48
49impl From<&str> for SystemSubtype {
50 fn from(s: &str) -> Self {
51 match s {
52 "init" => Self::Init,
53 "status" => Self::Status,
54 "compact_boundary" => Self::CompactBoundary,
55 "thinking_tokens" => Self::ThinkingTokens,
56 "task_started" => Self::TaskStarted,
57 "task_progress" => Self::TaskProgress,
58 "task_updated" => Self::TaskUpdated,
59 "task_notification" => Self::TaskNotification,
60 other => Self::Unknown(other.to_string()),
61 }
62 }
63}
64
65impl Serialize for SystemSubtype {
66 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
67 serializer.serialize_str(self.as_str())
68 }
69}
70
71impl<'de> Deserialize<'de> for SystemSubtype {
72 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
73 let s = String::deserialize(deserializer)?;
74 Ok(Self::from(s.as_str()))
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub enum MessageRole {
84 User,
85 Assistant,
86 Unknown(String),
88}
89
90impl MessageRole {
91 pub fn as_str(&self) -> &str {
92 match self {
93 Self::User => "user",
94 Self::Assistant => "assistant",
95 Self::Unknown(s) => s.as_str(),
96 }
97 }
98}
99
100impl fmt::Display for MessageRole {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(self.as_str())
103 }
104}
105
106impl From<&str> for MessageRole {
107 fn from(s: &str) -> Self {
108 match s {
109 "user" => Self::User,
110 "assistant" => Self::Assistant,
111 other => Self::Unknown(other.to_string()),
112 }
113 }
114}
115
116impl Serialize for MessageRole {
117 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
118 serializer.serialize_str(self.as_str())
119 }
120}
121
122impl<'de> Deserialize<'de> for MessageRole {
123 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
124 let s = String::deserialize(deserializer)?;
125 Ok(Self::from(s.as_str()))
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131pub enum CompactionTrigger {
132 Auto,
134 Manual,
136 Unknown(String),
138}
139
140impl CompactionTrigger {
141 pub fn as_str(&self) -> &str {
142 match self {
143 Self::Auto => "auto",
144 Self::Manual => "manual",
145 Self::Unknown(s) => s.as_str(),
146 }
147 }
148}
149
150impl fmt::Display for CompactionTrigger {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.write_str(self.as_str())
153 }
154}
155
156impl From<&str> for CompactionTrigger {
157 fn from(s: &str) -> Self {
158 match s {
159 "auto" => Self::Auto,
160 "manual" => Self::Manual,
161 other => Self::Unknown(other.to_string()),
162 }
163 }
164}
165
166impl Serialize for CompactionTrigger {
167 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
168 serializer.serialize_str(self.as_str())
169 }
170}
171
172impl<'de> Deserialize<'de> for CompactionTrigger {
173 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
174 let s = String::deserialize(deserializer)?;
175 Ok(Self::from(s.as_str()))
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Hash)]
181pub enum StopReason {
182 EndTurn,
184 MaxTokens,
186 ToolUse,
188 Unknown(String),
190}
191
192impl StopReason {
193 pub fn as_str(&self) -> &str {
194 match self {
195 Self::EndTurn => "end_turn",
196 Self::MaxTokens => "max_tokens",
197 Self::ToolUse => "tool_use",
198 Self::Unknown(s) => s.as_str(),
199 }
200 }
201}
202
203impl fmt::Display for StopReason {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 f.write_str(self.as_str())
206 }
207}
208
209impl From<&str> for StopReason {
210 fn from(s: &str) -> Self {
211 match s {
212 "end_turn" => Self::EndTurn,
213 "max_tokens" => Self::MaxTokens,
214 "tool_use" => Self::ToolUse,
215 other => Self::Unknown(other.to_string()),
216 }
217 }
218}
219
220impl Serialize for StopReason {
221 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
222 serializer.serialize_str(self.as_str())
223 }
224}
225
226impl<'de> Deserialize<'de> for StopReason {
227 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
228 let s = String::deserialize(deserializer)?;
229 Ok(Self::from(s.as_str()))
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Hash)]
235pub enum ApiKeySource {
236 None,
238 Unknown(String),
240}
241
242impl ApiKeySource {
243 pub fn as_str(&self) -> &str {
244 match self {
245 Self::None => "none",
246 Self::Unknown(s) => s.as_str(),
247 }
248 }
249}
250
251impl fmt::Display for ApiKeySource {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 f.write_str(self.as_str())
254 }
255}
256
257impl From<&str> for ApiKeySource {
258 fn from(s: &str) -> Self {
259 match s {
260 "none" => Self::None,
261 other => Self::Unknown(other.to_string()),
262 }
263 }
264}
265
266impl Serialize for ApiKeySource {
267 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
268 serializer.serialize_str(self.as_str())
269 }
270}
271
272impl<'de> Deserialize<'de> for ApiKeySource {
273 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
274 let s = String::deserialize(deserializer)?;
275 Ok(Self::from(s.as_str()))
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Hash)]
281pub enum OutputStyle {
282 Default,
284 Unknown(String),
286}
287
288impl OutputStyle {
289 pub fn as_str(&self) -> &str {
290 match self {
291 Self::Default => "default",
292 Self::Unknown(s) => s.as_str(),
293 }
294 }
295}
296
297impl fmt::Display for OutputStyle {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 f.write_str(self.as_str())
300 }
301}
302
303impl From<&str> for OutputStyle {
304 fn from(s: &str) -> Self {
305 match s {
306 "default" => Self::Default,
307 other => Self::Unknown(other.to_string()),
308 }
309 }
310}
311
312impl Serialize for OutputStyle {
313 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
314 serializer.serialize_str(self.as_str())
315 }
316}
317
318impl<'de> Deserialize<'de> for OutputStyle {
319 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
320 let s = String::deserialize(deserializer)?;
321 Ok(Self::from(s.as_str()))
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Hash)]
327pub enum InitPermissionMode {
328 Default,
330 Unknown(String),
332}
333
334impl InitPermissionMode {
335 pub fn as_str(&self) -> &str {
336 match self {
337 Self::Default => "default",
338 Self::Unknown(s) => s.as_str(),
339 }
340 }
341}
342
343impl fmt::Display for InitPermissionMode {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 f.write_str(self.as_str())
346 }
347}
348
349impl From<&str> for InitPermissionMode {
350 fn from(s: &str) -> Self {
351 match s {
352 "default" => Self::Default,
353 other => Self::Unknown(other.to_string()),
354 }
355 }
356}
357
358impl Serialize for InitPermissionMode {
359 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
360 serializer.serialize_str(self.as_str())
361 }
362}
363
364impl<'de> Deserialize<'de> for InitPermissionMode {
365 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
366 let s = String::deserialize(deserializer)?;
367 Ok(Self::from(s.as_str()))
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Hash)]
373pub enum StatusMessageStatus {
374 Compacting,
376 Unknown(String),
378}
379
380impl StatusMessageStatus {
381 pub fn as_str(&self) -> &str {
382 match self {
383 Self::Compacting => "compacting",
384 Self::Unknown(s) => s.as_str(),
385 }
386 }
387}
388
389impl fmt::Display for StatusMessageStatus {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 f.write_str(self.as_str())
392 }
393}
394
395impl From<&str> for StatusMessageStatus {
396 fn from(s: &str) -> Self {
397 match s {
398 "compacting" => Self::Compacting,
399 other => Self::Unknown(other.to_string()),
400 }
401 }
402}
403
404impl Serialize for StatusMessageStatus {
405 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
406 serializer.serialize_str(self.as_str())
407 }
408}
409
410impl<'de> Deserialize<'de> for StatusMessageStatus {
411 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
412 let s = String::deserialize(deserializer)?;
413 Ok(Self::from(s.as_str()))
414 }
415}
416
417pub(crate) fn serialize_optional_uuid<S>(
419 uuid: &Option<Uuid>,
420 serializer: S,
421) -> Result<S::Ok, S::Error>
422where
423 S: Serializer,
424{
425 match uuid {
426 Some(id) => serializer.serialize_str(&id.to_string()),
427 None => serializer.serialize_none(),
428 }
429}
430
431pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
433where
434 D: Deserializer<'de>,
435{
436 let opt_str: Option<String> = Option::deserialize(deserializer)?;
437 match opt_str {
438 Some(s) => Uuid::parse_str(&s)
439 .map(Some)
440 .map_err(serde::de::Error::custom),
441 None => Ok(None),
442 }
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct UserMessage {
448 pub message: MessageContent,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 #[serde(
451 serialize_with = "serialize_optional_uuid",
452 deserialize_with = "deserialize_optional_uuid"
453 )]
454 pub session_id: Option<Uuid>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub parent_tool_use_id: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub uuid: Option<String>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub timestamp: Option<String>,
464 #[serde(skip_serializing_if = "Option::is_none")]
471 pub tool_use_result: Option<serde_json::Value>,
472 #[serde(skip_serializing_if = "Option::is_none")]
475 pub subagent_type: Option<String>,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub task_description: Option<String>,
479}
480
481impl UserMessage {
482 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
490 &self,
491 ) -> Option<Result<T, serde_json::Error>> {
492 self.tool_use_result
493 .as_ref()
494 .map(|v| serde_json::from_value(v.clone()))
495 }
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct MessageContent {
501 pub role: MessageRole,
502 #[serde(deserialize_with = "deserialize_content_blocks")]
503 pub content: Vec<ContentBlock>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct SystemMessage {
509 pub subtype: SystemSubtype,
510 #[serde(flatten)]
511 pub data: Value, }
513
514impl SystemMessage {
515 pub fn is_init(&self) -> bool {
517 self.subtype == SystemSubtype::Init
518 }
519
520 pub fn is_status(&self) -> bool {
522 self.subtype == SystemSubtype::Status
523 }
524
525 pub fn is_compact_boundary(&self) -> bool {
527 self.subtype == SystemSubtype::CompactBoundary
528 }
529
530 pub fn as_init(&self) -> Option<InitMessage> {
532 if self.subtype != SystemSubtype::Init {
533 return None;
534 }
535 serde_json::from_value(self.data.clone()).ok()
536 }
537
538 pub fn as_status(&self) -> Option<StatusMessage> {
540 if self.subtype != SystemSubtype::Status {
541 return None;
542 }
543 serde_json::from_value(self.data.clone()).ok()
544 }
545
546 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
548 if self.subtype != SystemSubtype::CompactBoundary {
549 return None;
550 }
551 serde_json::from_value(self.data.clone()).ok()
552 }
553
554 pub fn is_task_started(&self) -> bool {
556 self.subtype == SystemSubtype::TaskStarted
557 }
558
559 pub fn is_task_progress(&self) -> bool {
561 self.subtype == SystemSubtype::TaskProgress
562 }
563
564 pub fn is_task_notification(&self) -> bool {
566 self.subtype == SystemSubtype::TaskNotification
567 }
568
569 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
571 if self.subtype != SystemSubtype::TaskStarted {
572 return None;
573 }
574 serde_json::from_value(self.data.clone()).ok()
575 }
576
577 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
579 if self.subtype != SystemSubtype::TaskProgress {
580 return None;
581 }
582 serde_json::from_value(self.data.clone()).ok()
583 }
584
585 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
587 if self.subtype != SystemSubtype::TaskNotification {
588 return None;
589 }
590 serde_json::from_value(self.data.clone()).ok()
591 }
592
593 pub fn is_task_updated(&self) -> bool {
595 self.subtype == SystemSubtype::TaskUpdated
596 }
597
598 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
600 if self.subtype != SystemSubtype::TaskUpdated {
601 return None;
602 }
603 serde_json::from_value(self.data.clone()).ok()
604 }
605
606 pub fn is_thinking_tokens(&self) -> bool {
608 self.subtype == SystemSubtype::ThinkingTokens
609 }
610
611 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
613 if self.subtype != SystemSubtype::ThinkingTokens {
614 return None;
615 }
616 serde_json::from_value(self.data.clone()).ok()
617 }
618
619 pub fn typed_value(&self) -> Option<Value> {
628 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
629 parsed.and_then(|v| serde_json::to_value(v).ok())
630 }
631 match self.subtype {
632 SystemSubtype::Init => reserialize(self.as_init()),
633 SystemSubtype::Status => reserialize(self.as_status()),
634 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
635 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
636 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
637 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
638 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
639 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
640 SystemSubtype::Unknown(_) => None,
641 }
642 }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize)]
647pub struct PluginInfo {
648 pub name: String,
650 pub path: String,
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub source: Option<String>,
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize)]
659pub struct InitMessage {
660 pub session_id: String,
662 #[serde(skip_serializing_if = "Option::is_none")]
664 pub cwd: Option<String>,
665 #[serde(skip_serializing_if = "Option::is_none")]
667 pub model: Option<String>,
668 #[serde(default, skip_serializing_if = "Vec::is_empty")]
670 pub tools: Vec<String>,
671 #[serde(default, skip_serializing_if = "Vec::is_empty")]
673 pub mcp_servers: Vec<Value>,
674 #[serde(default, skip_serializing_if = "Vec::is_empty")]
676 pub slash_commands: Vec<String>,
677 #[serde(default, skip_serializing_if = "Vec::is_empty")]
679 pub agents: Vec<String>,
680 #[serde(default, skip_serializing_if = "Vec::is_empty")]
682 pub plugins: Vec<PluginInfo>,
683 #[serde(default, skip_serializing_if = "Vec::is_empty")]
685 pub skills: Vec<Value>,
686 #[serde(skip_serializing_if = "Option::is_none")]
688 pub claude_code_version: Option<String>,
689 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
691 pub api_key_source: Option<ApiKeySource>,
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub output_style: Option<OutputStyle>,
695 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
697 pub permission_mode: Option<InitPermissionMode>,
698
699 #[serde(skip_serializing_if = "Option::is_none")]
701 pub uuid: Option<String>,
702
703 #[serde(skip_serializing_if = "Option::is_none")]
705 pub memory_paths: Option<Value>,
706
707 #[serde(skip_serializing_if = "Option::is_none")]
709 pub fast_mode_state: Option<String>,
710
711 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub analytics_disabled: Option<bool>,
714
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub product_feedback_disabled: Option<bool>,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize)]
722pub struct StatusMessage {
723 pub session_id: String,
725 pub status: Option<StatusMessageStatus>,
727 #[serde(skip_serializing_if = "Option::is_none")]
729 pub uuid: Option<String>,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct CompactBoundaryMessage {
735 pub session_id: String,
737 pub compact_metadata: CompactMetadata,
739 #[serde(
743 default,
744 skip_serializing_if = "Option::is_none",
745 alias = "content",
746 alias = "text"
747 )]
748 pub summary: Option<String>,
749 #[serde(
753 default,
754 skip_serializing_if = "Option::is_none",
755 alias = "message_count"
756 )]
757 pub leaf_message_count: Option<u32>,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub duration_ms: Option<u64>,
761 #[serde(skip_serializing_if = "Option::is_none")]
763 pub uuid: Option<String>,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize)]
768pub struct CompactMetadata {
769 pub pre_tokens: u64,
771 pub trigger: CompactionTrigger,
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct TaskUsage {
782 pub duration_ms: u64,
784 pub tool_uses: u64,
786 pub total_tokens: u64,
788}
789
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
792#[serde(rename_all = "snake_case")]
793pub enum TaskType {
794 LocalAgent,
796 LocalBash,
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
802#[serde(rename_all = "snake_case")]
803pub enum TaskStatus {
804 Completed,
805 Failed,
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize)]
810pub struct TaskStartedMessage {
811 pub session_id: String,
812 pub task_id: String,
813 pub task_type: TaskType,
814 pub tool_use_id: String,
815 pub description: String,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub subagent_type: Option<String>,
820 #[serde(default, skip_serializing_if = "Option::is_none")]
822 pub prompt: Option<String>,
823 pub uuid: String,
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct TaskUpdatedMessage {
831 pub session_id: String,
832 pub task_id: String,
833 pub patch: TaskPatch,
834 pub uuid: String,
835}
836
837#[derive(Debug, Clone, Default, Serialize, Deserialize)]
840pub struct TaskPatch {
841 #[serde(default, skip_serializing_if = "Option::is_none")]
842 pub status: Option<TaskStatus>,
843 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub end_time: Option<u64>,
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
852pub struct ThinkingTokensMessage {
853 pub session_id: String,
854 pub estimated_tokens: u64,
856 pub estimated_tokens_delta: u64,
858 pub uuid: String,
859}
860
861#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct TaskProgressMessage {
865 pub session_id: String,
866 pub task_id: String,
867 pub tool_use_id: String,
868 pub description: String,
869 pub last_tool_name: String,
870 pub usage: TaskUsage,
871 #[serde(default, skip_serializing_if = "Option::is_none")]
873 pub subagent_type: Option<String>,
874 pub uuid: String,
875}
876
877#[derive(Debug, Clone, Serialize, Deserialize)]
880pub struct TaskNotificationMessage {
881 pub session_id: String,
882 pub task_id: String,
883 pub status: TaskStatus,
884 pub summary: String,
885 pub output_file: Option<String>,
886 #[serde(skip_serializing_if = "Option::is_none")]
887 pub tool_use_id: Option<String>,
888 #[serde(skip_serializing_if = "Option::is_none")]
889 pub usage: Option<TaskUsage>,
890 #[serde(skip_serializing_if = "Option::is_none")]
891 pub uuid: Option<String>,
892}
893
894#[derive(Debug, Clone, Serialize, Deserialize)]
896pub struct AssistantMessage {
897 pub message: AssistantMessageContent,
898 pub session_id: String,
899 #[serde(skip_serializing_if = "Option::is_none")]
900 pub uuid: Option<String>,
901 #[serde(skip_serializing_if = "Option::is_none")]
902 pub parent_tool_use_id: Option<String>,
903 #[serde(skip_serializing_if = "Option::is_none")]
905 pub request_id: Option<String>,
906 #[serde(skip_serializing_if = "Option::is_none")]
909 pub subagent_type: Option<String>,
910 #[serde(skip_serializing_if = "Option::is_none")]
912 pub task_description: Option<String>,
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct AssistantMessageContent {
918 pub id: String,
919 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
921 pub message_type: Option<String>,
922 pub role: MessageRole,
923 pub model: String,
924 pub content: Vec<ContentBlock>,
925 #[serde(skip_serializing_if = "Option::is_none")]
926 pub stop_reason: Option<StopReason>,
927 #[serde(skip_serializing_if = "Option::is_none")]
928 pub stop_sequence: Option<String>,
929 #[serde(skip_serializing_if = "Option::is_none")]
930 pub usage: Option<AssistantUsage>,
931 #[serde(skip_serializing_if = "Option::is_none")]
933 pub stop_details: Option<Value>,
934 #[serde(skip_serializing_if = "Option::is_none")]
936 pub context_management: Option<Value>,
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize)]
941pub struct AssistantUsage {
942 #[serde(default)]
944 pub input_tokens: u32,
945
946 #[serde(default)]
948 pub output_tokens: u32,
949
950 #[serde(default)]
952 pub cache_creation_input_tokens: u32,
953
954 #[serde(default)]
956 pub cache_read_input_tokens: u32,
957
958 #[serde(skip_serializing_if = "Option::is_none")]
960 pub service_tier: Option<String>,
961
962 #[serde(skip_serializing_if = "Option::is_none")]
964 pub cache_creation: Option<CacheCreationDetails>,
965
966 #[serde(skip_serializing_if = "Option::is_none")]
968 pub inference_geo: Option<String>,
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize)]
973pub struct CacheCreationDetails {
974 #[serde(default)]
976 pub ephemeral_1h_input_tokens: u32,
977
978 #[serde(default)]
980 pub ephemeral_5m_input_tokens: u32,
981}
982
983#[cfg(test)]
984mod tests {
985 use crate::io::ClaudeOutput;
986
987 #[test]
988 fn test_system_message_init() {
989 let json = r#"{
990 "type": "system",
991 "subtype": "init",
992 "session_id": "test-session-123",
993 "cwd": "/home/user/project",
994 "model": "claude-sonnet-4",
995 "tools": ["Bash", "Read", "Write"],
996 "mcp_servers": [],
997 "slash_commands": ["compact", "cost", "review"],
998 "agents": ["Bash", "Explore", "Plan"],
999 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
1000 "skills": [],
1001 "claude_code_version": "2.1.15",
1002 "apiKeySource": "none",
1003 "output_style": "default",
1004 "permissionMode": "default"
1005 }"#;
1006
1007 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1008 if let ClaudeOutput::System(sys) = output {
1009 assert!(sys.is_init());
1010 assert!(!sys.is_status());
1011 assert!(!sys.is_compact_boundary());
1012
1013 let init = sys.as_init().expect("Should parse as init");
1014 assert_eq!(init.session_id, "test-session-123");
1015 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
1016 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
1017 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
1018 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
1019 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
1020 assert_eq!(init.plugins.len(), 1);
1021 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
1022 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
1023 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
1024 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
1025 assert_eq!(
1026 init.permission_mode,
1027 Some(super::InitPermissionMode::Default)
1028 );
1029 } else {
1030 panic!("Expected System message");
1031 }
1032 }
1033
1034 #[test]
1035 fn test_system_message_init_from_real_capture() {
1036 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
1037 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1038 if let ClaudeOutput::System(sys) = output {
1039 let init = sys.as_init().expect("Should parse real init capture");
1040 assert_eq!(init.slash_commands.len(), 8);
1041 assert!(init.slash_commands.contains(&"compact".to_string()));
1042 assert!(init.slash_commands.contains(&"review".to_string()));
1043 assert_eq!(init.agents.len(), 5);
1044 assert!(init.agents.contains(&"Bash".to_string()));
1045 assert!(init.agents.contains(&"Explore".to_string()));
1046 assert_eq!(init.plugins.len(), 1);
1047 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
1048 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
1049 } else {
1050 panic!("Expected System message");
1051 }
1052 }
1053
1054 #[test]
1055 fn test_system_message_status() {
1056 let json = r#"{
1057 "type": "system",
1058 "subtype": "status",
1059 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1060 "status": "compacting",
1061 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
1062 }"#;
1063
1064 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1065 if let ClaudeOutput::System(sys) = output {
1066 assert!(sys.is_status());
1067 assert!(!sys.is_init());
1068
1069 let status = sys.as_status().expect("Should parse as status");
1070 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1071 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
1072 assert_eq!(
1073 status.uuid,
1074 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
1075 );
1076 } else {
1077 panic!("Expected System message");
1078 }
1079 }
1080
1081 #[test]
1082 fn test_system_message_status_null() {
1083 let json = r#"{
1084 "type": "system",
1085 "subtype": "status",
1086 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1087 "status": null,
1088 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
1089 }"#;
1090
1091 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1092 if let ClaudeOutput::System(sys) = output {
1093 let status = sys.as_status().expect("Should parse as status");
1094 assert_eq!(status.status, None);
1095 } else {
1096 panic!("Expected System message");
1097 }
1098 }
1099
1100 #[test]
1101 fn test_system_message_task_started() {
1102 let json = r#"{
1103 "type": "system",
1104 "subtype": "task_started",
1105 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
1106 "task_id": "b6daf3f",
1107 "task_type": "local_bash",
1108 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
1109 "description": "Wait for CI on PR #12",
1110 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
1111 }"#;
1112
1113 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1114 if let ClaudeOutput::System(sys) = output {
1115 assert!(sys.is_task_started());
1116 assert!(!sys.is_task_progress());
1117 assert!(!sys.is_task_notification());
1118
1119 let task = sys.as_task_started().expect("Should parse as task_started");
1120 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
1121 assert_eq!(task.task_id, "b6daf3f");
1122 assert_eq!(task.task_type, super::TaskType::LocalBash);
1123 assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
1124 assert_eq!(task.description, "Wait for CI on PR #12");
1125 } else {
1126 panic!("Expected System message");
1127 }
1128 }
1129
1130 #[test]
1131 fn test_system_message_task_started_agent() {
1132 let json = r#"{
1133 "type": "system",
1134 "subtype": "task_started",
1135 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1136 "task_id": "a4a7e0906e5fc64cc",
1137 "task_type": "local_agent",
1138 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
1139 "description": "Explore Scene/ArrayScene duplication",
1140 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
1141 }"#;
1142
1143 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1144 if let ClaudeOutput::System(sys) = output {
1145 let task = sys.as_task_started().expect("Should parse as task_started");
1146 assert_eq!(task.task_type, super::TaskType::LocalAgent);
1147 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
1148 } else {
1149 panic!("Expected System message");
1150 }
1151 }
1152
1153 #[test]
1154 fn test_system_message_task_progress() {
1155 let json = r#"{
1156 "type": "system",
1157 "subtype": "task_progress",
1158 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1159 "task_id": "a4a7e0906e5fc64cc",
1160 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
1161 "description": "Reading src/jplephem/chebyshev.rs",
1162 "last_tool_name": "Read",
1163 "usage": {
1164 "duration_ms": 13996,
1165 "tool_uses": 9,
1166 "total_tokens": 38779
1167 },
1168 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
1169 }"#;
1170
1171 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1172 if let ClaudeOutput::System(sys) = output {
1173 assert!(sys.is_task_progress());
1174 assert!(!sys.is_task_started());
1175
1176 let progress = sys
1177 .as_task_progress()
1178 .expect("Should parse as task_progress");
1179 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
1180 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
1181 assert_eq!(progress.last_tool_name, "Read");
1182 assert_eq!(progress.usage.duration_ms, 13996);
1183 assert_eq!(progress.usage.tool_uses, 9);
1184 assert_eq!(progress.usage.total_tokens, 38779);
1185 } else {
1186 panic!("Expected System message");
1187 }
1188 }
1189
1190 #[test]
1191 fn test_system_message_task_notification_completed() {
1192 let json = r#"{
1193 "type": "system",
1194 "subtype": "task_notification",
1195 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1196 "task_id": "a0ba761e9dc9c316f",
1197 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
1198 "status": "completed",
1199 "summary": "Agent \"Write Hipparcos data source doc\" completed",
1200 "output_file": "",
1201 "usage": {
1202 "duration_ms": 172300,
1203 "tool_uses": 11,
1204 "total_tokens": 42005
1205 },
1206 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
1207 }"#;
1208
1209 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1210 if let ClaudeOutput::System(sys) = output {
1211 assert!(sys.is_task_notification());
1212
1213 let notif = sys
1214 .as_task_notification()
1215 .expect("Should parse as task_notification");
1216 assert_eq!(notif.status, super::TaskStatus::Completed);
1217 assert_eq!(
1218 notif.summary,
1219 "Agent \"Write Hipparcos data source doc\" completed"
1220 );
1221 assert_eq!(notif.output_file, Some("".to_string()));
1222 assert_eq!(
1223 notif.tool_use_id,
1224 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
1225 );
1226 let usage = notif.usage.expect("Should have usage");
1227 assert_eq!(usage.duration_ms, 172300);
1228 assert_eq!(usage.tool_uses, 11);
1229 assert_eq!(usage.total_tokens, 42005);
1230 } else {
1231 panic!("Expected System message");
1232 }
1233 }
1234
1235 #[test]
1236 fn test_system_message_task_notification_failed_no_usage() {
1237 let json = r#"{
1238 "type": "system",
1239 "subtype": "task_notification",
1240 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
1241 "task_id": "b98f6a3",
1242 "status": "failed",
1243 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
1244 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
1245 }"#;
1246
1247 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1248 if let ClaudeOutput::System(sys) = output {
1249 let notif = sys
1250 .as_task_notification()
1251 .expect("Should parse as task_notification");
1252 assert_eq!(notif.status, super::TaskStatus::Failed);
1253 assert!(notif.tool_use_id.is_none());
1254 assert!(notif.usage.is_none());
1255 assert_eq!(
1256 notif.output_file,
1257 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
1258 );
1259 } else {
1260 panic!("Expected System message");
1261 }
1262 }
1263
1264 #[test]
1270 fn test_task_messages_roundtrip_through_value() {
1271 let cases = [
1272 r#"{"type":"system","subtype":"task_started","session_id":"s1",
1273 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
1274 "description":"Sleep 3s","uuid":"u1"}"#,
1275 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
1276 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
1277 "last_tool_name":"Bash",
1278 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
1279 "uuid":"u2"}"#,
1280 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
1281 "task_id":"t1","tool_use_id":"tu1","status":"completed",
1282 "summary":"done","output_file":"",
1283 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
1284 "uuid":"u3"}"#,
1285 ];
1286
1287 for json in cases {
1288 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1289 let value = serde_json::to_value(&output).unwrap();
1290 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
1291
1292 let ClaudeOutput::System(sys) = reparsed else {
1293 panic!("Expected System variant after round-trip");
1294 };
1295
1296 match sys.subtype {
1297 super::SystemSubtype::TaskStarted => {
1298 assert!(
1299 sys.as_task_started().is_some(),
1300 "as_task_started failed after round-trip"
1301 );
1302 }
1303 super::SystemSubtype::TaskProgress => {
1304 assert!(
1305 sys.as_task_progress().is_some(),
1306 "as_task_progress failed after round-trip"
1307 );
1308 }
1309 super::SystemSubtype::TaskNotification => {
1310 assert!(
1311 sys.as_task_notification().is_some(),
1312 "as_task_notification failed after round-trip"
1313 );
1314 }
1315 other => panic!("unexpected subtype after round-trip: {other:?}"),
1316 }
1317 }
1318 }
1319
1320 #[test]
1321 fn test_system_message_compact_boundary() {
1322 let json = r#"{
1323 "type": "system",
1324 "subtype": "compact_boundary",
1325 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1326 "compact_metadata": {
1327 "pre_tokens": 155285,
1328 "trigger": "auto"
1329 },
1330 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1331 }"#;
1332
1333 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1334 if let ClaudeOutput::System(sys) = output {
1335 assert!(sys.is_compact_boundary());
1336 assert!(!sys.is_init());
1337 assert!(!sys.is_status());
1338
1339 let compact = sys
1340 .as_compact_boundary()
1341 .expect("Should parse as compact_boundary");
1342 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1343 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1344 assert_eq!(
1345 compact.compact_metadata.trigger,
1346 super::CompactionTrigger::Auto
1347 );
1348 assert!(compact.summary.is_none());
1350 assert!(compact.leaf_message_count.is_none());
1351 assert!(compact.duration_ms.is_none());
1352 } else {
1353 panic!("Expected System message");
1354 }
1355 }
1356
1357 #[test]
1358 fn test_compact_boundary_with_summary_stats() {
1359 let json = r#"{
1361 "type": "system",
1362 "subtype": "compact_boundary",
1363 "session_id": "s1",
1364 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
1365 "summary": "Summarized the earlier exploration.",
1366 "leaf_message_count": 42,
1367 "duration_ms": 1234,
1368 "uuid": "u1"
1369 }"#;
1370 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1371 let ClaudeOutput::System(sys) = output else {
1372 panic!("Expected System message");
1373 };
1374 let compact = sys.as_compact_boundary().expect("compact_boundary");
1375 assert_eq!(
1376 compact.summary.as_deref(),
1377 Some("Summarized the earlier exploration.")
1378 );
1379 assert_eq!(compact.leaf_message_count, Some(42));
1380 assert_eq!(compact.duration_ms, Some(1234));
1381
1382 let json_alt = r#"{
1385 "type": "system",
1386 "subtype": "compact_boundary",
1387 "session_id": "s2",
1388 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
1389 "content": "alt-key summary",
1390 "message_count": 7
1391 }"#;
1392 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
1393 let ClaudeOutput::System(sys) = output else {
1394 panic!("Expected System message");
1395 };
1396 let compact = sys.as_compact_boundary().expect("compact_boundary");
1397 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
1398 assert_eq!(compact.leaf_message_count, Some(7));
1399 }
1400
1401 #[test]
1402 fn test_init_message_with_new_fields() {
1403 let json = r#"{
1404 "type": "system",
1405 "subtype": "init",
1406 "session_id": "test-session",
1407 "cwd": "/home/user",
1408 "model": "claude-opus-4-7",
1409 "tools": ["Bash"],
1410 "mcp_servers": [],
1411 "permissionMode": "default",
1412 "apiKeySource": "none",
1413 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
1414 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
1415 "fast_mode_state": "off",
1416 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
1417 "claude_code_version": "2.1.117"
1418 }"#;
1419
1420 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1421 if let ClaudeOutput::System(sys) = output {
1422 let init = sys.as_init().expect("Should parse as init");
1423 assert_eq!(
1424 init.uuid.as_deref(),
1425 Some("44841a0d-182d-493a-86b5-79800d3d9665")
1426 );
1427 assert!(init.memory_paths.is_some());
1428 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
1429 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
1430 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
1431 } else {
1432 panic!("Expected System message");
1433 }
1434 }
1435
1436 #[test]
1437 fn test_assistant_message_with_new_fields() {
1438 let json = r#"{
1439 "type": "assistant",
1440 "message": {
1441 "id": "msg_1",
1442 "type": "message",
1443 "role": "assistant",
1444 "model": "claude-opus-4-7",
1445 "content": [{"type": "text", "text": "Hello"}],
1446 "stop_reason": "end_turn",
1447 "stop_details": null,
1448 "context_management": null,
1449 "usage": {
1450 "input_tokens": 100,
1451 "output_tokens": 10,
1452 "cache_creation_input_tokens": 50,
1453 "cache_read_input_tokens": 0,
1454 "service_tier": "standard",
1455 "inference_geo": "not_available"
1456 }
1457 },
1458 "session_id": "abc",
1459 "uuid": "msg-uuid-123"
1460 }"#;
1461
1462 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1463 if let ClaudeOutput::Assistant(asst) = output {
1464 assert_eq!(asst.message.stop_details, None);
1465 assert_eq!(asst.message.context_management, None);
1466 let usage = asst.message.usage.unwrap();
1467 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1468 } else {
1469 panic!("Expected Assistant message");
1470 }
1471 }
1472
1473 #[test]
1474 fn test_user_message_with_new_fields() {
1475 let json = r#"{
1476 "type": "user",
1477 "message": {
1478 "role": "user",
1479 "content": [{"type": "text", "text": "Hello"}]
1480 },
1481 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
1482 "parent_tool_use_id": "toolu_123",
1483 "uuid": "user-msg-456"
1484 }"#;
1485
1486 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1487 if let ClaudeOutput::User(user) = output {
1488 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
1489 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
1490 } else {
1491 panic!("Expected User message");
1492 }
1493 }
1494
1495 #[test]
1501 fn test_user_message_preserves_tool_use_result_and_timestamp() {
1502 let json = r#"{
1503 "type":"user",
1504 "message":{"role":"user","content":[{"type":"tool_result","content":"User has answered your questions: . You can now continue with the user's answers in mind.","tool_use_id":"toolu_01331duMqP2PrRaqR2yWa8e4"}]},
1505 "parent_tool_use_id":null,
1506 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
1507 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
1508 "timestamp":"2026-05-12T23:12:04.121Z",
1509 "tool_use_result":{"questions":[{"question":"Which color do you prefer?","header":"Color","options":[{"label":"Red","description":"A warm color"},{"label":"Blue","description":"A cool color"}],"multiSelect":false}],"answers":{"Color":"Blue"}}
1510 }"#;
1511
1512 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1513 let user = match output {
1514 ClaudeOutput::User(u) => u,
1515 other => panic!("Expected User message, got {:?}", other.message_type()),
1516 };
1517
1518 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
1519 let raw = user
1520 .tool_use_result
1521 .as_ref()
1522 .expect("tool_use_result must be captured");
1523 assert_eq!(raw["answers"]["Color"], "Blue");
1524 assert_eq!(raw["questions"][0]["header"], "Color");
1525
1526 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
1530 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
1531 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
1532 assert_eq!(
1533 reser["tool_use_result"]["questions"][0]["question"],
1534 "Which color do you prefer?"
1535 );
1536
1537 let typed: crate::AskUserQuestionInput = user
1540 .tool_use_result_as::<crate::AskUserQuestionInput>()
1541 .expect("tool_use_result present")
1542 .expect("AskUserQuestionInput parses");
1543 assert_eq!(typed.questions.len(), 1);
1544 assert_eq!(typed.questions[0].header, "Color");
1545 let answers = typed.answers.expect("answers populated");
1546 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
1547 }
1548
1549 #[test]
1552 fn test_user_message_without_tool_use_result_omits_field() {
1553 let json = r#"{
1554 "type":"user",
1555 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
1556 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
1557 }"#;
1558
1559 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1560 let user = match output {
1561 ClaudeOutput::User(u) => u,
1562 _ => panic!("Expected User message"),
1563 };
1564 assert!(user.tool_use_result.is_none());
1565 assert!(user.timestamp.is_none());
1566
1567 let reser = serde_json::to_value(&user).unwrap();
1568 assert!(reser.get("tool_use_result").is_none());
1569 assert!(reser.get("timestamp").is_none());
1570 }
1571}