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}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct MessageContent {
460 pub role: MessageRole,
461 #[serde(deserialize_with = "deserialize_content_blocks")]
462 pub content: Vec<ContentBlock>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct SystemMessage {
468 pub subtype: SystemSubtype,
469 #[serde(flatten)]
470 pub data: Value, }
472
473impl SystemMessage {
474 pub fn is_init(&self) -> bool {
476 self.subtype == SystemSubtype::Init
477 }
478
479 pub fn is_status(&self) -> bool {
481 self.subtype == SystemSubtype::Status
482 }
483
484 pub fn is_compact_boundary(&self) -> bool {
486 self.subtype == SystemSubtype::CompactBoundary
487 }
488
489 pub fn as_init(&self) -> Option<InitMessage> {
491 if self.subtype != SystemSubtype::Init {
492 return None;
493 }
494 serde_json::from_value(self.data.clone()).ok()
495 }
496
497 pub fn as_status(&self) -> Option<StatusMessage> {
499 if self.subtype != SystemSubtype::Status {
500 return None;
501 }
502 serde_json::from_value(self.data.clone()).ok()
503 }
504
505 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
507 if self.subtype != SystemSubtype::CompactBoundary {
508 return None;
509 }
510 serde_json::from_value(self.data.clone()).ok()
511 }
512
513 pub fn is_task_started(&self) -> bool {
515 self.subtype == SystemSubtype::TaskStarted
516 }
517
518 pub fn is_task_progress(&self) -> bool {
520 self.subtype == SystemSubtype::TaskProgress
521 }
522
523 pub fn is_task_notification(&self) -> bool {
525 self.subtype == SystemSubtype::TaskNotification
526 }
527
528 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
530 if self.subtype != SystemSubtype::TaskStarted {
531 return None;
532 }
533 serde_json::from_value(self.data.clone()).ok()
534 }
535
536 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
538 if self.subtype != SystemSubtype::TaskProgress {
539 return None;
540 }
541 serde_json::from_value(self.data.clone()).ok()
542 }
543
544 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
546 if self.subtype != SystemSubtype::TaskNotification {
547 return None;
548 }
549 serde_json::from_value(self.data.clone()).ok()
550 }
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct PluginInfo {
556 pub name: String,
558 pub path: String,
560 #[serde(skip_serializing_if = "Option::is_none")]
562 pub source: Option<String>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct InitMessage {
568 pub session_id: String,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub cwd: Option<String>,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub model: Option<String>,
576 #[serde(default, skip_serializing_if = "Vec::is_empty")]
578 pub tools: Vec<String>,
579 #[serde(default, skip_serializing_if = "Vec::is_empty")]
581 pub mcp_servers: Vec<Value>,
582 #[serde(default, skip_serializing_if = "Vec::is_empty")]
584 pub slash_commands: Vec<String>,
585 #[serde(default, skip_serializing_if = "Vec::is_empty")]
587 pub agents: Vec<String>,
588 #[serde(default, skip_serializing_if = "Vec::is_empty")]
590 pub plugins: Vec<PluginInfo>,
591 #[serde(default, skip_serializing_if = "Vec::is_empty")]
593 pub skills: Vec<Value>,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub claude_code_version: Option<String>,
597 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
599 pub api_key_source: Option<ApiKeySource>,
600 #[serde(skip_serializing_if = "Option::is_none")]
602 pub output_style: Option<OutputStyle>,
603 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
605 pub permission_mode: Option<InitPermissionMode>,
606
607 #[serde(skip_serializing_if = "Option::is_none")]
609 pub uuid: Option<String>,
610
611 #[serde(skip_serializing_if = "Option::is_none")]
613 pub memory_paths: Option<Value>,
614
615 #[serde(skip_serializing_if = "Option::is_none")]
617 pub fast_mode_state: Option<String>,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct StatusMessage {
623 pub session_id: String,
625 pub status: Option<StatusMessageStatus>,
627 #[serde(skip_serializing_if = "Option::is_none")]
629 pub uuid: Option<String>,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct CompactBoundaryMessage {
635 pub session_id: String,
637 pub compact_metadata: CompactMetadata,
639 #[serde(skip_serializing_if = "Option::is_none")]
641 pub uuid: Option<String>,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct CompactMetadata {
647 pub pre_tokens: u64,
649 pub trigger: CompactionTrigger,
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize)]
659pub struct TaskUsage {
660 pub duration_ms: u64,
662 pub tool_uses: u64,
664 pub total_tokens: u64,
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
670#[serde(rename_all = "snake_case")]
671pub enum TaskType {
672 LocalAgent,
674 LocalBash,
676}
677
678#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
680#[serde(rename_all = "snake_case")]
681pub enum TaskStatus {
682 Completed,
683 Failed,
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct TaskStartedMessage {
689 pub session_id: String,
690 pub task_id: String,
691 pub task_type: TaskType,
692 pub tool_use_id: String,
693 pub description: String,
694 pub uuid: String,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct TaskProgressMessage {
701 pub session_id: String,
702 pub task_id: String,
703 pub tool_use_id: String,
704 pub description: String,
705 pub last_tool_name: String,
706 pub usage: TaskUsage,
707 pub uuid: String,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct TaskNotificationMessage {
714 pub session_id: String,
715 pub task_id: String,
716 pub status: TaskStatus,
717 pub summary: String,
718 pub output_file: Option<String>,
719 #[serde(skip_serializing_if = "Option::is_none")]
720 pub tool_use_id: Option<String>,
721 #[serde(skip_serializing_if = "Option::is_none")]
722 pub usage: Option<TaskUsage>,
723 #[serde(skip_serializing_if = "Option::is_none")]
724 pub uuid: Option<String>,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct AssistantMessage {
730 pub message: AssistantMessageContent,
731 pub session_id: String,
732 #[serde(skip_serializing_if = "Option::is_none")]
733 pub uuid: Option<String>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub parent_tool_use_id: Option<String>,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct AssistantMessageContent {
741 pub id: String,
742 pub role: MessageRole,
743 pub model: String,
744 pub content: Vec<ContentBlock>,
745 #[serde(skip_serializing_if = "Option::is_none")]
746 pub stop_reason: Option<StopReason>,
747 #[serde(skip_serializing_if = "Option::is_none")]
748 pub stop_sequence: Option<String>,
749 #[serde(skip_serializing_if = "Option::is_none")]
750 pub usage: Option<AssistantUsage>,
751 #[serde(skip_serializing_if = "Option::is_none")]
753 pub stop_details: Option<Value>,
754 #[serde(skip_serializing_if = "Option::is_none")]
756 pub context_management: Option<Value>,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct AssistantUsage {
762 #[serde(default)]
764 pub input_tokens: u32,
765
766 #[serde(default)]
768 pub output_tokens: u32,
769
770 #[serde(default)]
772 pub cache_creation_input_tokens: u32,
773
774 #[serde(default)]
776 pub cache_read_input_tokens: u32,
777
778 #[serde(skip_serializing_if = "Option::is_none")]
780 pub service_tier: Option<String>,
781
782 #[serde(skip_serializing_if = "Option::is_none")]
784 pub cache_creation: Option<CacheCreationDetails>,
785
786 #[serde(skip_serializing_if = "Option::is_none")]
788 pub inference_geo: Option<String>,
789}
790
791#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct CacheCreationDetails {
794 #[serde(default)]
796 pub ephemeral_1h_input_tokens: u32,
797
798 #[serde(default)]
800 pub ephemeral_5m_input_tokens: u32,
801}
802
803#[cfg(test)]
804mod tests {
805 use crate::io::ClaudeOutput;
806
807 #[test]
808 fn test_system_message_init() {
809 let json = r#"{
810 "type": "system",
811 "subtype": "init",
812 "session_id": "test-session-123",
813 "cwd": "/home/user/project",
814 "model": "claude-sonnet-4",
815 "tools": ["Bash", "Read", "Write"],
816 "mcp_servers": [],
817 "slash_commands": ["compact", "cost", "review"],
818 "agents": ["Bash", "Explore", "Plan"],
819 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
820 "skills": [],
821 "claude_code_version": "2.1.15",
822 "apiKeySource": "none",
823 "output_style": "default",
824 "permissionMode": "default"
825 }"#;
826
827 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
828 if let ClaudeOutput::System(sys) = output {
829 assert!(sys.is_init());
830 assert!(!sys.is_status());
831 assert!(!sys.is_compact_boundary());
832
833 let init = sys.as_init().expect("Should parse as init");
834 assert_eq!(init.session_id, "test-session-123");
835 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
836 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
837 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
838 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
839 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
840 assert_eq!(init.plugins.len(), 1);
841 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
842 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
843 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
844 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
845 assert_eq!(
846 init.permission_mode,
847 Some(super::InitPermissionMode::Default)
848 );
849 } else {
850 panic!("Expected System message");
851 }
852 }
853
854 #[test]
855 fn test_system_message_init_from_real_capture() {
856 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
857 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
858 if let ClaudeOutput::System(sys) = output {
859 let init = sys.as_init().expect("Should parse real init capture");
860 assert_eq!(init.slash_commands.len(), 8);
861 assert!(init.slash_commands.contains(&"compact".to_string()));
862 assert!(init.slash_commands.contains(&"review".to_string()));
863 assert_eq!(init.agents.len(), 5);
864 assert!(init.agents.contains(&"Bash".to_string()));
865 assert!(init.agents.contains(&"Explore".to_string()));
866 assert_eq!(init.plugins.len(), 1);
867 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
868 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
869 } else {
870 panic!("Expected System message");
871 }
872 }
873
874 #[test]
875 fn test_system_message_status() {
876 let json = r#"{
877 "type": "system",
878 "subtype": "status",
879 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
880 "status": "compacting",
881 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
882 }"#;
883
884 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
885 if let ClaudeOutput::System(sys) = output {
886 assert!(sys.is_status());
887 assert!(!sys.is_init());
888
889 let status = sys.as_status().expect("Should parse as status");
890 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
891 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
892 assert_eq!(
893 status.uuid,
894 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
895 );
896 } else {
897 panic!("Expected System message");
898 }
899 }
900
901 #[test]
902 fn test_system_message_status_null() {
903 let json = r#"{
904 "type": "system",
905 "subtype": "status",
906 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
907 "status": null,
908 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
909 }"#;
910
911 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
912 if let ClaudeOutput::System(sys) = output {
913 let status = sys.as_status().expect("Should parse as status");
914 assert_eq!(status.status, None);
915 } else {
916 panic!("Expected System message");
917 }
918 }
919
920 #[test]
921 fn test_system_message_task_started() {
922 let json = r#"{
923 "type": "system",
924 "subtype": "task_started",
925 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
926 "task_id": "b6daf3f",
927 "task_type": "local_bash",
928 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
929 "description": "Wait for CI on PR #12",
930 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
931 }"#;
932
933 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
934 if let ClaudeOutput::System(sys) = output {
935 assert!(sys.is_task_started());
936 assert!(!sys.is_task_progress());
937 assert!(!sys.is_task_notification());
938
939 let task = sys.as_task_started().expect("Should parse as task_started");
940 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
941 assert_eq!(task.task_id, "b6daf3f");
942 assert_eq!(task.task_type, super::TaskType::LocalBash);
943 assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
944 assert_eq!(task.description, "Wait for CI on PR #12");
945 } else {
946 panic!("Expected System message");
947 }
948 }
949
950 #[test]
951 fn test_system_message_task_started_agent() {
952 let json = r#"{
953 "type": "system",
954 "subtype": "task_started",
955 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
956 "task_id": "a4a7e0906e5fc64cc",
957 "task_type": "local_agent",
958 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
959 "description": "Explore Scene/ArrayScene duplication",
960 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
961 }"#;
962
963 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
964 if let ClaudeOutput::System(sys) = output {
965 let task = sys.as_task_started().expect("Should parse as task_started");
966 assert_eq!(task.task_type, super::TaskType::LocalAgent);
967 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
968 } else {
969 panic!("Expected System message");
970 }
971 }
972
973 #[test]
974 fn test_system_message_task_progress() {
975 let json = r#"{
976 "type": "system",
977 "subtype": "task_progress",
978 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
979 "task_id": "a4a7e0906e5fc64cc",
980 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
981 "description": "Reading src/jplephem/chebyshev.rs",
982 "last_tool_name": "Read",
983 "usage": {
984 "duration_ms": 13996,
985 "tool_uses": 9,
986 "total_tokens": 38779
987 },
988 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
989 }"#;
990
991 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
992 if let ClaudeOutput::System(sys) = output {
993 assert!(sys.is_task_progress());
994 assert!(!sys.is_task_started());
995
996 let progress = sys
997 .as_task_progress()
998 .expect("Should parse as task_progress");
999 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
1000 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
1001 assert_eq!(progress.last_tool_name, "Read");
1002 assert_eq!(progress.usage.duration_ms, 13996);
1003 assert_eq!(progress.usage.tool_uses, 9);
1004 assert_eq!(progress.usage.total_tokens, 38779);
1005 } else {
1006 panic!("Expected System message");
1007 }
1008 }
1009
1010 #[test]
1011 fn test_system_message_task_notification_completed() {
1012 let json = r#"{
1013 "type": "system",
1014 "subtype": "task_notification",
1015 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1016 "task_id": "a0ba761e9dc9c316f",
1017 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
1018 "status": "completed",
1019 "summary": "Agent \"Write Hipparcos data source doc\" completed",
1020 "output_file": "",
1021 "usage": {
1022 "duration_ms": 172300,
1023 "tool_uses": 11,
1024 "total_tokens": 42005
1025 },
1026 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
1027 }"#;
1028
1029 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1030 if let ClaudeOutput::System(sys) = output {
1031 assert!(sys.is_task_notification());
1032
1033 let notif = sys
1034 .as_task_notification()
1035 .expect("Should parse as task_notification");
1036 assert_eq!(notif.status, super::TaskStatus::Completed);
1037 assert_eq!(
1038 notif.summary,
1039 "Agent \"Write Hipparcos data source doc\" completed"
1040 );
1041 assert_eq!(notif.output_file, Some("".to_string()));
1042 assert_eq!(
1043 notif.tool_use_id,
1044 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
1045 );
1046 let usage = notif.usage.expect("Should have usage");
1047 assert_eq!(usage.duration_ms, 172300);
1048 assert_eq!(usage.tool_uses, 11);
1049 assert_eq!(usage.total_tokens, 42005);
1050 } else {
1051 panic!("Expected System message");
1052 }
1053 }
1054
1055 #[test]
1056 fn test_system_message_task_notification_failed_no_usage() {
1057 let json = r#"{
1058 "type": "system",
1059 "subtype": "task_notification",
1060 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
1061 "task_id": "b98f6a3",
1062 "status": "failed",
1063 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
1064 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
1065 }"#;
1066
1067 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1068 if let ClaudeOutput::System(sys) = output {
1069 let notif = sys
1070 .as_task_notification()
1071 .expect("Should parse as task_notification");
1072 assert_eq!(notif.status, super::TaskStatus::Failed);
1073 assert!(notif.tool_use_id.is_none());
1074 assert!(notif.usage.is_none());
1075 assert_eq!(
1076 notif.output_file,
1077 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
1078 );
1079 } else {
1080 panic!("Expected System message");
1081 }
1082 }
1083
1084 #[test]
1085 fn test_system_message_compact_boundary() {
1086 let json = r#"{
1087 "type": "system",
1088 "subtype": "compact_boundary",
1089 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1090 "compact_metadata": {
1091 "pre_tokens": 155285,
1092 "trigger": "auto"
1093 },
1094 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1095 }"#;
1096
1097 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1098 if let ClaudeOutput::System(sys) = output {
1099 assert!(sys.is_compact_boundary());
1100 assert!(!sys.is_init());
1101 assert!(!sys.is_status());
1102
1103 let compact = sys
1104 .as_compact_boundary()
1105 .expect("Should parse as compact_boundary");
1106 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1107 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1108 assert_eq!(
1109 compact.compact_metadata.trigger,
1110 super::CompactionTrigger::Auto
1111 );
1112 } else {
1113 panic!("Expected System message");
1114 }
1115 }
1116
1117 #[test]
1118 fn test_init_message_with_new_fields() {
1119 let json = r#"{
1120 "type": "system",
1121 "subtype": "init",
1122 "session_id": "test-session",
1123 "cwd": "/home/user",
1124 "model": "claude-opus-4-7",
1125 "tools": ["Bash"],
1126 "mcp_servers": [],
1127 "permissionMode": "default",
1128 "apiKeySource": "none",
1129 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
1130 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
1131 "fast_mode_state": "off",
1132 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
1133 "claude_code_version": "2.1.117"
1134 }"#;
1135
1136 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1137 if let ClaudeOutput::System(sys) = output {
1138 let init = sys.as_init().expect("Should parse as init");
1139 assert_eq!(
1140 init.uuid.as_deref(),
1141 Some("44841a0d-182d-493a-86b5-79800d3d9665")
1142 );
1143 assert!(init.memory_paths.is_some());
1144 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
1145 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
1146 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
1147 } else {
1148 panic!("Expected System message");
1149 }
1150 }
1151
1152 #[test]
1153 fn test_assistant_message_with_new_fields() {
1154 let json = r#"{
1155 "type": "assistant",
1156 "message": {
1157 "id": "msg_1",
1158 "type": "message",
1159 "role": "assistant",
1160 "model": "claude-opus-4-7",
1161 "content": [{"type": "text", "text": "Hello"}],
1162 "stop_reason": "end_turn",
1163 "stop_details": null,
1164 "context_management": null,
1165 "usage": {
1166 "input_tokens": 100,
1167 "output_tokens": 10,
1168 "cache_creation_input_tokens": 50,
1169 "cache_read_input_tokens": 0,
1170 "service_tier": "standard",
1171 "inference_geo": "not_available"
1172 }
1173 },
1174 "session_id": "abc",
1175 "uuid": "msg-uuid-123"
1176 }"#;
1177
1178 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1179 if let ClaudeOutput::Assistant(asst) = output {
1180 assert_eq!(asst.message.stop_details, None);
1181 assert_eq!(asst.message.context_management, None);
1182 let usage = asst.message.usage.unwrap();
1183 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1184 } else {
1185 panic!("Expected Assistant message");
1186 }
1187 }
1188
1189 #[test]
1190 fn test_user_message_with_new_fields() {
1191 let json = r#"{
1192 "type": "user",
1193 "message": {
1194 "role": "user",
1195 "content": [{"type": "text", "text": "Hello"}]
1196 },
1197 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
1198 "parent_tool_use_id": "toolu_123",
1199 "uuid": "user-msg-456"
1200 }"#;
1201
1202 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1203 if let ClaudeOutput::User(user) = output {
1204 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
1205 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
1206 } else {
1207 panic!("Expected User message");
1208 }
1209 }
1210}