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 TaskStarted,
19 TaskProgress,
20 TaskNotification,
21 Unknown(String),
23}
24
25impl SystemSubtype {
26 pub fn as_str(&self) -> &str {
27 match self {
28 Self::Init => "init",
29 Self::Status => "status",
30 Self::CompactBoundary => "compact_boundary",
31 Self::TaskStarted => "task_started",
32 Self::TaskProgress => "task_progress",
33 Self::TaskNotification => "task_notification",
34 Self::Unknown(s) => s.as_str(),
35 }
36 }
37}
38
39impl fmt::Display for SystemSubtype {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(self.as_str())
42 }
43}
44
45impl From<&str> for SystemSubtype {
46 fn from(s: &str) -> Self {
47 match s {
48 "init" => Self::Init,
49 "status" => Self::Status,
50 "compact_boundary" => Self::CompactBoundary,
51 "task_started" => Self::TaskStarted,
52 "task_progress" => Self::TaskProgress,
53 "task_notification" => Self::TaskNotification,
54 other => Self::Unknown(other.to_string()),
55 }
56 }
57}
58
59impl Serialize for SystemSubtype {
60 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
61 serializer.serialize_str(self.as_str())
62 }
63}
64
65impl<'de> Deserialize<'de> for SystemSubtype {
66 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
67 let s = String::deserialize(deserializer)?;
68 Ok(Self::from(s.as_str()))
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub enum MessageRole {
78 User,
79 Assistant,
80 Unknown(String),
82}
83
84impl MessageRole {
85 pub fn as_str(&self) -> &str {
86 match self {
87 Self::User => "user",
88 Self::Assistant => "assistant",
89 Self::Unknown(s) => s.as_str(),
90 }
91 }
92}
93
94impl fmt::Display for MessageRole {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100impl From<&str> for MessageRole {
101 fn from(s: &str) -> Self {
102 match s {
103 "user" => Self::User,
104 "assistant" => Self::Assistant,
105 other => Self::Unknown(other.to_string()),
106 }
107 }
108}
109
110impl Serialize for MessageRole {
111 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112 serializer.serialize_str(self.as_str())
113 }
114}
115
116impl<'de> Deserialize<'de> for MessageRole {
117 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
118 let s = String::deserialize(deserializer)?;
119 Ok(Self::from(s.as_str()))
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub enum CompactionTrigger {
126 Auto,
128 Manual,
130 Unknown(String),
132}
133
134impl CompactionTrigger {
135 pub fn as_str(&self) -> &str {
136 match self {
137 Self::Auto => "auto",
138 Self::Manual => "manual",
139 Self::Unknown(s) => s.as_str(),
140 }
141 }
142}
143
144impl fmt::Display for CompactionTrigger {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.write_str(self.as_str())
147 }
148}
149
150impl From<&str> for CompactionTrigger {
151 fn from(s: &str) -> Self {
152 match s {
153 "auto" => Self::Auto,
154 "manual" => Self::Manual,
155 other => Self::Unknown(other.to_string()),
156 }
157 }
158}
159
160impl Serialize for CompactionTrigger {
161 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
162 serializer.serialize_str(self.as_str())
163 }
164}
165
166impl<'de> Deserialize<'de> for CompactionTrigger {
167 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168 let s = String::deserialize(deserializer)?;
169 Ok(Self::from(s.as_str()))
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Hash)]
175pub enum StopReason {
176 EndTurn,
178 MaxTokens,
180 ToolUse,
182 Unknown(String),
184}
185
186impl StopReason {
187 pub fn as_str(&self) -> &str {
188 match self {
189 Self::EndTurn => "end_turn",
190 Self::MaxTokens => "max_tokens",
191 Self::ToolUse => "tool_use",
192 Self::Unknown(s) => s.as_str(),
193 }
194 }
195}
196
197impl fmt::Display for StopReason {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 f.write_str(self.as_str())
200 }
201}
202
203impl From<&str> for StopReason {
204 fn from(s: &str) -> Self {
205 match s {
206 "end_turn" => Self::EndTurn,
207 "max_tokens" => Self::MaxTokens,
208 "tool_use" => Self::ToolUse,
209 other => Self::Unknown(other.to_string()),
210 }
211 }
212}
213
214impl Serialize for StopReason {
215 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
216 serializer.serialize_str(self.as_str())
217 }
218}
219
220impl<'de> Deserialize<'de> for StopReason {
221 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
222 let s = String::deserialize(deserializer)?;
223 Ok(Self::from(s.as_str()))
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub enum ApiKeySource {
230 None,
232 Unknown(String),
234}
235
236impl ApiKeySource {
237 pub fn as_str(&self) -> &str {
238 match self {
239 Self::None => "none",
240 Self::Unknown(s) => s.as_str(),
241 }
242 }
243}
244
245impl fmt::Display for ApiKeySource {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.write_str(self.as_str())
248 }
249}
250
251impl From<&str> for ApiKeySource {
252 fn from(s: &str) -> Self {
253 match s {
254 "none" => Self::None,
255 other => Self::Unknown(other.to_string()),
256 }
257 }
258}
259
260impl Serialize for ApiKeySource {
261 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
262 serializer.serialize_str(self.as_str())
263 }
264}
265
266impl<'de> Deserialize<'de> for ApiKeySource {
267 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
268 let s = String::deserialize(deserializer)?;
269 Ok(Self::from(s.as_str()))
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Hash)]
275pub enum OutputStyle {
276 Default,
278 Unknown(String),
280}
281
282impl OutputStyle {
283 pub fn as_str(&self) -> &str {
284 match self {
285 Self::Default => "default",
286 Self::Unknown(s) => s.as_str(),
287 }
288 }
289}
290
291impl fmt::Display for OutputStyle {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 f.write_str(self.as_str())
294 }
295}
296
297impl From<&str> for OutputStyle {
298 fn from(s: &str) -> Self {
299 match s {
300 "default" => Self::Default,
301 other => Self::Unknown(other.to_string()),
302 }
303 }
304}
305
306impl Serialize for OutputStyle {
307 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
308 serializer.serialize_str(self.as_str())
309 }
310}
311
312impl<'de> Deserialize<'de> for OutputStyle {
313 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
314 let s = String::deserialize(deserializer)?;
315 Ok(Self::from(s.as_str()))
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Hash)]
321pub enum InitPermissionMode {
322 Default,
324 Unknown(String),
326}
327
328impl InitPermissionMode {
329 pub fn as_str(&self) -> &str {
330 match self {
331 Self::Default => "default",
332 Self::Unknown(s) => s.as_str(),
333 }
334 }
335}
336
337impl fmt::Display for InitPermissionMode {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 f.write_str(self.as_str())
340 }
341}
342
343impl From<&str> for InitPermissionMode {
344 fn from(s: &str) -> Self {
345 match s {
346 "default" => Self::Default,
347 other => Self::Unknown(other.to_string()),
348 }
349 }
350}
351
352impl Serialize for InitPermissionMode {
353 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
354 serializer.serialize_str(self.as_str())
355 }
356}
357
358impl<'de> Deserialize<'de> for InitPermissionMode {
359 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
360 let s = String::deserialize(deserializer)?;
361 Ok(Self::from(s.as_str()))
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Hash)]
367pub enum StatusMessageStatus {
368 Compacting,
370 Unknown(String),
372}
373
374impl StatusMessageStatus {
375 pub fn as_str(&self) -> &str {
376 match self {
377 Self::Compacting => "compacting",
378 Self::Unknown(s) => s.as_str(),
379 }
380 }
381}
382
383impl fmt::Display for StatusMessageStatus {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 f.write_str(self.as_str())
386 }
387}
388
389impl From<&str> for StatusMessageStatus {
390 fn from(s: &str) -> Self {
391 match s {
392 "compacting" => Self::Compacting,
393 other => Self::Unknown(other.to_string()),
394 }
395 }
396}
397
398impl Serialize for StatusMessageStatus {
399 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
400 serializer.serialize_str(self.as_str())
401 }
402}
403
404impl<'de> Deserialize<'de> for StatusMessageStatus {
405 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
406 let s = String::deserialize(deserializer)?;
407 Ok(Self::from(s.as_str()))
408 }
409}
410
411pub(crate) fn serialize_optional_uuid<S>(
413 uuid: &Option<Uuid>,
414 serializer: S,
415) -> Result<S::Ok, S::Error>
416where
417 S: Serializer,
418{
419 match uuid {
420 Some(id) => serializer.serialize_str(&id.to_string()),
421 None => serializer.serialize_none(),
422 }
423}
424
425pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
427where
428 D: Deserializer<'de>,
429{
430 let opt_str: Option<String> = Option::deserialize(deserializer)?;
431 match opt_str {
432 Some(s) => Uuid::parse_str(&s)
433 .map(Some)
434 .map_err(serde::de::Error::custom),
435 None => Ok(None),
436 }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct UserMessage {
442 pub message: MessageContent,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 #[serde(
445 serialize_with = "serialize_optional_uuid",
446 deserialize_with = "deserialize_optional_uuid"
447 )]
448 pub session_id: Option<Uuid>,
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub parent_tool_use_id: Option<String>,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub uuid: Option<String>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub timestamp: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
465 pub tool_use_result: Option<serde_json::Value>,
466}
467
468impl UserMessage {
469 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
477 &self,
478 ) -> Option<Result<T, serde_json::Error>> {
479 self.tool_use_result
480 .as_ref()
481 .map(|v| serde_json::from_value(v.clone()))
482 }
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct MessageContent {
488 pub role: MessageRole,
489 #[serde(deserialize_with = "deserialize_content_blocks")]
490 pub content: Vec<ContentBlock>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct SystemMessage {
496 pub subtype: SystemSubtype,
497 #[serde(flatten)]
498 pub data: Value, }
500
501impl SystemMessage {
502 pub fn is_init(&self) -> bool {
504 self.subtype == SystemSubtype::Init
505 }
506
507 pub fn is_status(&self) -> bool {
509 self.subtype == SystemSubtype::Status
510 }
511
512 pub fn is_compact_boundary(&self) -> bool {
514 self.subtype == SystemSubtype::CompactBoundary
515 }
516
517 pub fn as_init(&self) -> Option<InitMessage> {
519 if self.subtype != SystemSubtype::Init {
520 return None;
521 }
522 serde_json::from_value(self.data.clone()).ok()
523 }
524
525 pub fn as_status(&self) -> Option<StatusMessage> {
527 if self.subtype != SystemSubtype::Status {
528 return None;
529 }
530 serde_json::from_value(self.data.clone()).ok()
531 }
532
533 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
535 if self.subtype != SystemSubtype::CompactBoundary {
536 return None;
537 }
538 serde_json::from_value(self.data.clone()).ok()
539 }
540
541 pub fn is_task_started(&self) -> bool {
543 self.subtype == SystemSubtype::TaskStarted
544 }
545
546 pub fn is_task_progress(&self) -> bool {
548 self.subtype == SystemSubtype::TaskProgress
549 }
550
551 pub fn is_task_notification(&self) -> bool {
553 self.subtype == SystemSubtype::TaskNotification
554 }
555
556 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
558 if self.subtype != SystemSubtype::TaskStarted {
559 return None;
560 }
561 serde_json::from_value(self.data.clone()).ok()
562 }
563
564 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
566 if self.subtype != SystemSubtype::TaskProgress {
567 return None;
568 }
569 serde_json::from_value(self.data.clone()).ok()
570 }
571
572 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
574 if self.subtype != SystemSubtype::TaskNotification {
575 return None;
576 }
577 serde_json::from_value(self.data.clone()).ok()
578 }
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct PluginInfo {
584 pub name: String,
586 pub path: String,
588 #[serde(skip_serializing_if = "Option::is_none")]
590 pub source: Option<String>,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct InitMessage {
596 pub session_id: String,
598 #[serde(skip_serializing_if = "Option::is_none")]
600 pub cwd: Option<String>,
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub model: Option<String>,
604 #[serde(default, skip_serializing_if = "Vec::is_empty")]
606 pub tools: Vec<String>,
607 #[serde(default, skip_serializing_if = "Vec::is_empty")]
609 pub mcp_servers: Vec<Value>,
610 #[serde(default, skip_serializing_if = "Vec::is_empty")]
612 pub slash_commands: Vec<String>,
613 #[serde(default, skip_serializing_if = "Vec::is_empty")]
615 pub agents: Vec<String>,
616 #[serde(default, skip_serializing_if = "Vec::is_empty")]
618 pub plugins: Vec<PluginInfo>,
619 #[serde(default, skip_serializing_if = "Vec::is_empty")]
621 pub skills: Vec<Value>,
622 #[serde(skip_serializing_if = "Option::is_none")]
624 pub claude_code_version: Option<String>,
625 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
627 pub api_key_source: Option<ApiKeySource>,
628 #[serde(skip_serializing_if = "Option::is_none")]
630 pub output_style: Option<OutputStyle>,
631 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
633 pub permission_mode: Option<InitPermissionMode>,
634
635 #[serde(skip_serializing_if = "Option::is_none")]
637 pub uuid: Option<String>,
638
639 #[serde(skip_serializing_if = "Option::is_none")]
641 pub memory_paths: Option<Value>,
642
643 #[serde(skip_serializing_if = "Option::is_none")]
645 pub fast_mode_state: Option<String>,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct StatusMessage {
651 pub session_id: String,
653 pub status: Option<StatusMessageStatus>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub uuid: Option<String>,
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct CompactBoundaryMessage {
663 pub session_id: String,
665 pub compact_metadata: CompactMetadata,
667 #[serde(
671 default,
672 skip_serializing_if = "Option::is_none",
673 alias = "content",
674 alias = "text"
675 )]
676 pub summary: Option<String>,
677 #[serde(
681 default,
682 skip_serializing_if = "Option::is_none",
683 alias = "message_count"
684 )]
685 pub leaf_message_count: Option<u32>,
686 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub duration_ms: Option<u64>,
689 #[serde(skip_serializing_if = "Option::is_none")]
691 pub uuid: Option<String>,
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize)]
696pub struct CompactMetadata {
697 pub pre_tokens: u64,
699 pub trigger: CompactionTrigger,
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct TaskUsage {
710 pub duration_ms: u64,
712 pub tool_uses: u64,
714 pub total_tokens: u64,
716}
717
718#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720#[serde(rename_all = "snake_case")]
721pub enum TaskType {
722 LocalAgent,
724 LocalBash,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
730#[serde(rename_all = "snake_case")]
731pub enum TaskStatus {
732 Completed,
733 Failed,
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
738pub struct TaskStartedMessage {
739 pub session_id: String,
740 pub task_id: String,
741 pub task_type: TaskType,
742 pub tool_use_id: String,
743 pub description: String,
744 pub uuid: String,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct TaskProgressMessage {
751 pub session_id: String,
752 pub task_id: String,
753 pub tool_use_id: String,
754 pub description: String,
755 pub last_tool_name: String,
756 pub usage: TaskUsage,
757 pub uuid: String,
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize)]
763pub struct TaskNotificationMessage {
764 pub session_id: String,
765 pub task_id: String,
766 pub status: TaskStatus,
767 pub summary: String,
768 pub output_file: Option<String>,
769 #[serde(skip_serializing_if = "Option::is_none")]
770 pub tool_use_id: Option<String>,
771 #[serde(skip_serializing_if = "Option::is_none")]
772 pub usage: Option<TaskUsage>,
773 #[serde(skip_serializing_if = "Option::is_none")]
774 pub uuid: Option<String>,
775}
776
777#[derive(Debug, Clone, Serialize, Deserialize)]
779pub struct AssistantMessage {
780 pub message: AssistantMessageContent,
781 pub session_id: String,
782 #[serde(skip_serializing_if = "Option::is_none")]
783 pub uuid: Option<String>,
784 #[serde(skip_serializing_if = "Option::is_none")]
785 pub parent_tool_use_id: Option<String>,
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize)]
790pub struct AssistantMessageContent {
791 pub id: String,
792 pub role: MessageRole,
793 pub model: String,
794 pub content: Vec<ContentBlock>,
795 #[serde(skip_serializing_if = "Option::is_none")]
796 pub stop_reason: Option<StopReason>,
797 #[serde(skip_serializing_if = "Option::is_none")]
798 pub stop_sequence: Option<String>,
799 #[serde(skip_serializing_if = "Option::is_none")]
800 pub usage: Option<AssistantUsage>,
801 #[serde(skip_serializing_if = "Option::is_none")]
803 pub stop_details: Option<Value>,
804 #[serde(skip_serializing_if = "Option::is_none")]
806 pub context_management: Option<Value>,
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize)]
811pub struct AssistantUsage {
812 #[serde(default)]
814 pub input_tokens: u32,
815
816 #[serde(default)]
818 pub output_tokens: u32,
819
820 #[serde(default)]
822 pub cache_creation_input_tokens: u32,
823
824 #[serde(default)]
826 pub cache_read_input_tokens: u32,
827
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub service_tier: Option<String>,
831
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub cache_creation: Option<CacheCreationDetails>,
835
836 #[serde(skip_serializing_if = "Option::is_none")]
838 pub inference_geo: Option<String>,
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct CacheCreationDetails {
844 #[serde(default)]
846 pub ephemeral_1h_input_tokens: u32,
847
848 #[serde(default)]
850 pub ephemeral_5m_input_tokens: u32,
851}
852
853#[cfg(test)]
854mod tests {
855 use crate::io::ClaudeOutput;
856
857 #[test]
858 fn test_system_message_init() {
859 let json = r#"{
860 "type": "system",
861 "subtype": "init",
862 "session_id": "test-session-123",
863 "cwd": "/home/user/project",
864 "model": "claude-sonnet-4",
865 "tools": ["Bash", "Read", "Write"],
866 "mcp_servers": [],
867 "slash_commands": ["compact", "cost", "review"],
868 "agents": ["Bash", "Explore", "Plan"],
869 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
870 "skills": [],
871 "claude_code_version": "2.1.15",
872 "apiKeySource": "none",
873 "output_style": "default",
874 "permissionMode": "default"
875 }"#;
876
877 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
878 if let ClaudeOutput::System(sys) = output {
879 assert!(sys.is_init());
880 assert!(!sys.is_status());
881 assert!(!sys.is_compact_boundary());
882
883 let init = sys.as_init().expect("Should parse as init");
884 assert_eq!(init.session_id, "test-session-123");
885 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
886 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
887 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
888 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
889 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
890 assert_eq!(init.plugins.len(), 1);
891 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
892 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
893 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
894 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
895 assert_eq!(
896 init.permission_mode,
897 Some(super::InitPermissionMode::Default)
898 );
899 } else {
900 panic!("Expected System message");
901 }
902 }
903
904 #[test]
905 fn test_system_message_init_from_real_capture() {
906 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
907 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
908 if let ClaudeOutput::System(sys) = output {
909 let init = sys.as_init().expect("Should parse real init capture");
910 assert_eq!(init.slash_commands.len(), 8);
911 assert!(init.slash_commands.contains(&"compact".to_string()));
912 assert!(init.slash_commands.contains(&"review".to_string()));
913 assert_eq!(init.agents.len(), 5);
914 assert!(init.agents.contains(&"Bash".to_string()));
915 assert!(init.agents.contains(&"Explore".to_string()));
916 assert_eq!(init.plugins.len(), 1);
917 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
918 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
919 } else {
920 panic!("Expected System message");
921 }
922 }
923
924 #[test]
925 fn test_system_message_status() {
926 let json = r#"{
927 "type": "system",
928 "subtype": "status",
929 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
930 "status": "compacting",
931 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
932 }"#;
933
934 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
935 if let ClaudeOutput::System(sys) = output {
936 assert!(sys.is_status());
937 assert!(!sys.is_init());
938
939 let status = sys.as_status().expect("Should parse as status");
940 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
941 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
942 assert_eq!(
943 status.uuid,
944 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
945 );
946 } else {
947 panic!("Expected System message");
948 }
949 }
950
951 #[test]
952 fn test_system_message_status_null() {
953 let json = r#"{
954 "type": "system",
955 "subtype": "status",
956 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
957 "status": null,
958 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
959 }"#;
960
961 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
962 if let ClaudeOutput::System(sys) = output {
963 let status = sys.as_status().expect("Should parse as status");
964 assert_eq!(status.status, None);
965 } else {
966 panic!("Expected System message");
967 }
968 }
969
970 #[test]
971 fn test_system_message_task_started() {
972 let json = r#"{
973 "type": "system",
974 "subtype": "task_started",
975 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
976 "task_id": "b6daf3f",
977 "task_type": "local_bash",
978 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
979 "description": "Wait for CI on PR #12",
980 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
981 }"#;
982
983 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
984 if let ClaudeOutput::System(sys) = output {
985 assert!(sys.is_task_started());
986 assert!(!sys.is_task_progress());
987 assert!(!sys.is_task_notification());
988
989 let task = sys.as_task_started().expect("Should parse as task_started");
990 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
991 assert_eq!(task.task_id, "b6daf3f");
992 assert_eq!(task.task_type, super::TaskType::LocalBash);
993 assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
994 assert_eq!(task.description, "Wait for CI on PR #12");
995 } else {
996 panic!("Expected System message");
997 }
998 }
999
1000 #[test]
1001 fn test_system_message_task_started_agent() {
1002 let json = r#"{
1003 "type": "system",
1004 "subtype": "task_started",
1005 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1006 "task_id": "a4a7e0906e5fc64cc",
1007 "task_type": "local_agent",
1008 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
1009 "description": "Explore Scene/ArrayScene duplication",
1010 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
1011 }"#;
1012
1013 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1014 if let ClaudeOutput::System(sys) = output {
1015 let task = sys.as_task_started().expect("Should parse as task_started");
1016 assert_eq!(task.task_type, super::TaskType::LocalAgent);
1017 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
1018 } else {
1019 panic!("Expected System message");
1020 }
1021 }
1022
1023 #[test]
1024 fn test_system_message_task_progress() {
1025 let json = r#"{
1026 "type": "system",
1027 "subtype": "task_progress",
1028 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1029 "task_id": "a4a7e0906e5fc64cc",
1030 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
1031 "description": "Reading src/jplephem/chebyshev.rs",
1032 "last_tool_name": "Read",
1033 "usage": {
1034 "duration_ms": 13996,
1035 "tool_uses": 9,
1036 "total_tokens": 38779
1037 },
1038 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
1039 }"#;
1040
1041 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1042 if let ClaudeOutput::System(sys) = output {
1043 assert!(sys.is_task_progress());
1044 assert!(!sys.is_task_started());
1045
1046 let progress = sys
1047 .as_task_progress()
1048 .expect("Should parse as task_progress");
1049 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
1050 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
1051 assert_eq!(progress.last_tool_name, "Read");
1052 assert_eq!(progress.usage.duration_ms, 13996);
1053 assert_eq!(progress.usage.tool_uses, 9);
1054 assert_eq!(progress.usage.total_tokens, 38779);
1055 } else {
1056 panic!("Expected System message");
1057 }
1058 }
1059
1060 #[test]
1061 fn test_system_message_task_notification_completed() {
1062 let json = r#"{
1063 "type": "system",
1064 "subtype": "task_notification",
1065 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1066 "task_id": "a0ba761e9dc9c316f",
1067 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
1068 "status": "completed",
1069 "summary": "Agent \"Write Hipparcos data source doc\" completed",
1070 "output_file": "",
1071 "usage": {
1072 "duration_ms": 172300,
1073 "tool_uses": 11,
1074 "total_tokens": 42005
1075 },
1076 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
1077 }"#;
1078
1079 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1080 if let ClaudeOutput::System(sys) = output {
1081 assert!(sys.is_task_notification());
1082
1083 let notif = sys
1084 .as_task_notification()
1085 .expect("Should parse as task_notification");
1086 assert_eq!(notif.status, super::TaskStatus::Completed);
1087 assert_eq!(
1088 notif.summary,
1089 "Agent \"Write Hipparcos data source doc\" completed"
1090 );
1091 assert_eq!(notif.output_file, Some("".to_string()));
1092 assert_eq!(
1093 notif.tool_use_id,
1094 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
1095 );
1096 let usage = notif.usage.expect("Should have usage");
1097 assert_eq!(usage.duration_ms, 172300);
1098 assert_eq!(usage.tool_uses, 11);
1099 assert_eq!(usage.total_tokens, 42005);
1100 } else {
1101 panic!("Expected System message");
1102 }
1103 }
1104
1105 #[test]
1106 fn test_system_message_task_notification_failed_no_usage() {
1107 let json = r#"{
1108 "type": "system",
1109 "subtype": "task_notification",
1110 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
1111 "task_id": "b98f6a3",
1112 "status": "failed",
1113 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
1114 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
1115 }"#;
1116
1117 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1118 if let ClaudeOutput::System(sys) = output {
1119 let notif = sys
1120 .as_task_notification()
1121 .expect("Should parse as task_notification");
1122 assert_eq!(notif.status, super::TaskStatus::Failed);
1123 assert!(notif.tool_use_id.is_none());
1124 assert!(notif.usage.is_none());
1125 assert_eq!(
1126 notif.output_file,
1127 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
1128 );
1129 } else {
1130 panic!("Expected System message");
1131 }
1132 }
1133
1134 #[test]
1140 fn test_task_messages_roundtrip_through_value() {
1141 let cases = [
1142 r#"{"type":"system","subtype":"task_started","session_id":"s1",
1143 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
1144 "description":"Sleep 3s","uuid":"u1"}"#,
1145 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
1146 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
1147 "last_tool_name":"Bash",
1148 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
1149 "uuid":"u2"}"#,
1150 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
1151 "task_id":"t1","tool_use_id":"tu1","status":"completed",
1152 "summary":"done","output_file":"",
1153 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
1154 "uuid":"u3"}"#,
1155 ];
1156
1157 for json in cases {
1158 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1159 let value = serde_json::to_value(&output).unwrap();
1160 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
1161
1162 let ClaudeOutput::System(sys) = reparsed else {
1163 panic!("Expected System variant after round-trip");
1164 };
1165
1166 match sys.subtype {
1167 super::SystemSubtype::TaskStarted => {
1168 assert!(
1169 sys.as_task_started().is_some(),
1170 "as_task_started failed after round-trip"
1171 );
1172 }
1173 super::SystemSubtype::TaskProgress => {
1174 assert!(
1175 sys.as_task_progress().is_some(),
1176 "as_task_progress failed after round-trip"
1177 );
1178 }
1179 super::SystemSubtype::TaskNotification => {
1180 assert!(
1181 sys.as_task_notification().is_some(),
1182 "as_task_notification failed after round-trip"
1183 );
1184 }
1185 other => panic!("unexpected subtype after round-trip: {other:?}"),
1186 }
1187 }
1188 }
1189
1190 #[test]
1191 fn test_system_message_compact_boundary() {
1192 let json = r#"{
1193 "type": "system",
1194 "subtype": "compact_boundary",
1195 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1196 "compact_metadata": {
1197 "pre_tokens": 155285,
1198 "trigger": "auto"
1199 },
1200 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1201 }"#;
1202
1203 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1204 if let ClaudeOutput::System(sys) = output {
1205 assert!(sys.is_compact_boundary());
1206 assert!(!sys.is_init());
1207 assert!(!sys.is_status());
1208
1209 let compact = sys
1210 .as_compact_boundary()
1211 .expect("Should parse as compact_boundary");
1212 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1213 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1214 assert_eq!(
1215 compact.compact_metadata.trigger,
1216 super::CompactionTrigger::Auto
1217 );
1218 assert!(compact.summary.is_none());
1220 assert!(compact.leaf_message_count.is_none());
1221 assert!(compact.duration_ms.is_none());
1222 } else {
1223 panic!("Expected System message");
1224 }
1225 }
1226
1227 #[test]
1228 fn test_compact_boundary_with_summary_stats() {
1229 let json = r#"{
1231 "type": "system",
1232 "subtype": "compact_boundary",
1233 "session_id": "s1",
1234 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
1235 "summary": "Summarized the earlier exploration.",
1236 "leaf_message_count": 42,
1237 "duration_ms": 1234,
1238 "uuid": "u1"
1239 }"#;
1240 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1241 let ClaudeOutput::System(sys) = output else {
1242 panic!("Expected System message");
1243 };
1244 let compact = sys.as_compact_boundary().expect("compact_boundary");
1245 assert_eq!(
1246 compact.summary.as_deref(),
1247 Some("Summarized the earlier exploration.")
1248 );
1249 assert_eq!(compact.leaf_message_count, Some(42));
1250 assert_eq!(compact.duration_ms, Some(1234));
1251
1252 let json_alt = r#"{
1255 "type": "system",
1256 "subtype": "compact_boundary",
1257 "session_id": "s2",
1258 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
1259 "content": "alt-key summary",
1260 "message_count": 7
1261 }"#;
1262 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
1263 let ClaudeOutput::System(sys) = output else {
1264 panic!("Expected System message");
1265 };
1266 let compact = sys.as_compact_boundary().expect("compact_boundary");
1267 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
1268 assert_eq!(compact.leaf_message_count, Some(7));
1269 }
1270
1271 #[test]
1272 fn test_init_message_with_new_fields() {
1273 let json = r#"{
1274 "type": "system",
1275 "subtype": "init",
1276 "session_id": "test-session",
1277 "cwd": "/home/user",
1278 "model": "claude-opus-4-7",
1279 "tools": ["Bash"],
1280 "mcp_servers": [],
1281 "permissionMode": "default",
1282 "apiKeySource": "none",
1283 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
1284 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
1285 "fast_mode_state": "off",
1286 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
1287 "claude_code_version": "2.1.117"
1288 }"#;
1289
1290 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1291 if let ClaudeOutput::System(sys) = output {
1292 let init = sys.as_init().expect("Should parse as init");
1293 assert_eq!(
1294 init.uuid.as_deref(),
1295 Some("44841a0d-182d-493a-86b5-79800d3d9665")
1296 );
1297 assert!(init.memory_paths.is_some());
1298 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
1299 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
1300 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
1301 } else {
1302 panic!("Expected System message");
1303 }
1304 }
1305
1306 #[test]
1307 fn test_assistant_message_with_new_fields() {
1308 let json = r#"{
1309 "type": "assistant",
1310 "message": {
1311 "id": "msg_1",
1312 "type": "message",
1313 "role": "assistant",
1314 "model": "claude-opus-4-7",
1315 "content": [{"type": "text", "text": "Hello"}],
1316 "stop_reason": "end_turn",
1317 "stop_details": null,
1318 "context_management": null,
1319 "usage": {
1320 "input_tokens": 100,
1321 "output_tokens": 10,
1322 "cache_creation_input_tokens": 50,
1323 "cache_read_input_tokens": 0,
1324 "service_tier": "standard",
1325 "inference_geo": "not_available"
1326 }
1327 },
1328 "session_id": "abc",
1329 "uuid": "msg-uuid-123"
1330 }"#;
1331
1332 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1333 if let ClaudeOutput::Assistant(asst) = output {
1334 assert_eq!(asst.message.stop_details, None);
1335 assert_eq!(asst.message.context_management, None);
1336 let usage = asst.message.usage.unwrap();
1337 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1338 } else {
1339 panic!("Expected Assistant message");
1340 }
1341 }
1342
1343 #[test]
1344 fn test_user_message_with_new_fields() {
1345 let json = r#"{
1346 "type": "user",
1347 "message": {
1348 "role": "user",
1349 "content": [{"type": "text", "text": "Hello"}]
1350 },
1351 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
1352 "parent_tool_use_id": "toolu_123",
1353 "uuid": "user-msg-456"
1354 }"#;
1355
1356 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1357 if let ClaudeOutput::User(user) = output {
1358 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
1359 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
1360 } else {
1361 panic!("Expected User message");
1362 }
1363 }
1364
1365 #[test]
1371 fn test_user_message_preserves_tool_use_result_and_timestamp() {
1372 let json = r#"{
1373 "type":"user",
1374 "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"}]},
1375 "parent_tool_use_id":null,
1376 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
1377 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
1378 "timestamp":"2026-05-12T23:12:04.121Z",
1379 "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"}}
1380 }"#;
1381
1382 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1383 let user = match output {
1384 ClaudeOutput::User(u) => u,
1385 other => panic!("Expected User message, got {:?}", other.message_type()),
1386 };
1387
1388 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
1389 let raw = user
1390 .tool_use_result
1391 .as_ref()
1392 .expect("tool_use_result must be captured");
1393 assert_eq!(raw["answers"]["Color"], "Blue");
1394 assert_eq!(raw["questions"][0]["header"], "Color");
1395
1396 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
1400 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
1401 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
1402 assert_eq!(
1403 reser["tool_use_result"]["questions"][0]["question"],
1404 "Which color do you prefer?"
1405 );
1406
1407 let typed: crate::AskUserQuestionInput = user
1410 .tool_use_result_as::<crate::AskUserQuestionInput>()
1411 .expect("tool_use_result present")
1412 .expect("AskUserQuestionInput parses");
1413 assert_eq!(typed.questions.len(), 1);
1414 assert_eq!(typed.questions[0].header, "Color");
1415 let answers = typed.answers.expect("answers populated");
1416 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
1417 }
1418
1419 #[test]
1422 fn test_user_message_without_tool_use_result_omits_field() {
1423 let json = r#"{
1424 "type":"user",
1425 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
1426 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
1427 }"#;
1428
1429 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1430 let user = match output {
1431 ClaudeOutput::User(u) => u,
1432 _ => panic!("Expected User message"),
1433 };
1434 assert!(user.tool_use_result.is_none());
1435 assert!(user.timestamp.is_none());
1436
1437 let reser = serde_json::to_value(&user).unwrap();
1438 assert!(reser.get("tool_use_result").is_none());
1439 assert!(reser.get("timestamp").is_none());
1440 }
1441}