Skip to main content

claude_codes/io/
message_types.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use uuid::Uuid;
4
5use super::content_blocks::{deserialize_content_blocks, ContentBlock};
6
7/// Serialize an optional UUID as a string
8pub(crate) fn serialize_optional_uuid<S>(
9    uuid: &Option<Uuid>,
10    serializer: S,
11) -> Result<S::Ok, S::Error>
12where
13    S: Serializer,
14{
15    match uuid {
16        Some(id) => serializer.serialize_str(&id.to_string()),
17        None => serializer.serialize_none(),
18    }
19}
20
21/// Deserialize an optional UUID from a string
22pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
23where
24    D: Deserializer<'de>,
25{
26    let opt_str: Option<String> = Option::deserialize(deserializer)?;
27    match opt_str {
28        Some(s) => Uuid::parse_str(&s)
29            .map(Some)
30            .map_err(serde::de::Error::custom),
31        None => Ok(None),
32    }
33}
34
35/// User message
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct UserMessage {
38    pub message: MessageContent,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    #[serde(
41        serialize_with = "serialize_optional_uuid",
42        deserialize_with = "deserialize_optional_uuid"
43    )]
44    pub session_id: Option<Uuid>,
45}
46
47/// Message content with role
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct MessageContent {
50    pub role: String,
51    #[serde(deserialize_with = "deserialize_content_blocks")]
52    pub content: Vec<ContentBlock>,
53}
54
55/// System message with metadata
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SystemMessage {
58    pub subtype: String,
59    #[serde(flatten)]
60    pub data: Value, // Captures all other fields
61}
62
63impl SystemMessage {
64    /// Check if this is an init message
65    pub fn is_init(&self) -> bool {
66        self.subtype == "init"
67    }
68
69    /// Check if this is a status message
70    pub fn is_status(&self) -> bool {
71        self.subtype == "status"
72    }
73
74    /// Check if this is a compact_boundary message
75    pub fn is_compact_boundary(&self) -> bool {
76        self.subtype == "compact_boundary"
77    }
78
79    /// Try to parse as an init message
80    pub fn as_init(&self) -> Option<InitMessage> {
81        if self.subtype != "init" {
82            return None;
83        }
84        serde_json::from_value(self.data.clone()).ok()
85    }
86
87    /// Try to parse as a status message
88    pub fn as_status(&self) -> Option<StatusMessage> {
89        if self.subtype != "status" {
90            return None;
91        }
92        serde_json::from_value(self.data.clone()).ok()
93    }
94
95    /// Try to parse as a compact_boundary message
96    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
97        if self.subtype != "compact_boundary" {
98            return None;
99        }
100        serde_json::from_value(self.data.clone()).ok()
101    }
102
103    /// Check if this is a task_started message
104    pub fn is_task_started(&self) -> bool {
105        self.subtype == "task_started"
106    }
107
108    /// Check if this is a task_progress message
109    pub fn is_task_progress(&self) -> bool {
110        self.subtype == "task_progress"
111    }
112
113    /// Check if this is a task_notification message
114    pub fn is_task_notification(&self) -> bool {
115        self.subtype == "task_notification"
116    }
117
118    /// Try to parse as a task_started message
119    pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
120        if self.subtype != "task_started" {
121            return None;
122        }
123        serde_json::from_value(self.data.clone()).ok()
124    }
125
126    /// Try to parse as a task_progress message
127    pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
128        if self.subtype != "task_progress" {
129            return None;
130        }
131        serde_json::from_value(self.data.clone()).ok()
132    }
133
134    /// Try to parse as a task_notification message
135    pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
136        if self.subtype != "task_notification" {
137            return None;
138        }
139        serde_json::from_value(self.data.clone()).ok()
140    }
141}
142
143/// Plugin info from the init message
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct PluginInfo {
146    /// Plugin name
147    pub name: String,
148    /// Path to the plugin on disk
149    pub path: String,
150}
151
152/// Init system message data - sent at session start
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct InitMessage {
155    /// Session identifier
156    pub session_id: String,
157    /// Current working directory
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub cwd: Option<String>,
160    /// Model being used
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub model: Option<String>,
163    /// List of available tools
164    #[serde(default, skip_serializing_if = "Vec::is_empty")]
165    pub tools: Vec<String>,
166    /// MCP servers configured
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    pub mcp_servers: Vec<Value>,
169    /// Available slash commands (e.g., "compact", "cost", "review")
170    #[serde(default, skip_serializing_if = "Vec::is_empty")]
171    pub slash_commands: Vec<String>,
172    /// Available agent types (e.g., "Bash", "Explore", "Plan")
173    #[serde(default, skip_serializing_if = "Vec::is_empty")]
174    pub agents: Vec<String>,
175    /// Installed plugins
176    #[serde(default, skip_serializing_if = "Vec::is_empty")]
177    pub plugins: Vec<PluginInfo>,
178    /// Installed skills
179    #[serde(default, skip_serializing_if = "Vec::is_empty")]
180    pub skills: Vec<Value>,
181    /// Claude Code CLI version
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub claude_code_version: Option<String>,
184    /// How the API key was sourced (e.g., "none")
185    #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
186    pub api_key_source: Option<String>,
187    /// Output style (e.g., "default")
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub output_style: Option<String>,
190    /// Permission mode (e.g., "default")
191    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
192    pub permission_mode: Option<String>,
193}
194
195/// Status system message - sent during operations like context compaction
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct StatusMessage {
198    /// Session identifier
199    pub session_id: String,
200    /// Current status (e.g., "compacting") or null when complete
201    pub status: Option<String>,
202    /// Unique identifier for this message
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub uuid: Option<String>,
205}
206
207/// Compact boundary message - marks where context compaction occurred
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct CompactBoundaryMessage {
210    /// Session identifier
211    pub session_id: String,
212    /// Metadata about the compaction
213    pub compact_metadata: CompactMetadata,
214    /// Unique identifier for this message
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub uuid: Option<String>,
217}
218
219/// Metadata about context compaction
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct CompactMetadata {
222    /// Number of tokens before compaction
223    pub pre_tokens: u64,
224    /// What triggered the compaction ("auto" or "manual")
225    pub trigger: String,
226}
227
228// ---------------------------------------------------------------------------
229// Task system message types (task_started, task_progress, task_notification)
230// ---------------------------------------------------------------------------
231
232/// Cumulative usage statistics for a background task.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct TaskUsage {
235    /// Wall-clock milliseconds since the task started.
236    pub duration_ms: u64,
237    /// Total number of tool calls made so far.
238    pub tool_uses: u64,
239    /// Total tokens consumed so far.
240    pub total_tokens: u64,
241}
242
243/// The kind of background task.
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum TaskType {
247    /// A sub-agent task (e.g., Explore, Plan).
248    LocalAgent,
249    /// A background bash command.
250    LocalBash,
251}
252
253/// Completion status of a background task.
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum TaskStatus {
257    Completed,
258    Failed,
259}
260
261/// `task_started` system message — emitted once when a background task begins.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct TaskStartedMessage {
264    pub session_id: String,
265    pub task_id: String,
266    pub task_type: TaskType,
267    pub tool_use_id: String,
268    pub description: String,
269    pub uuid: String,
270}
271
272/// `task_progress` system message — emitted periodically as a background
273/// agent task executes tools. Not emitted for `local_bash` tasks.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct TaskProgressMessage {
276    pub session_id: String,
277    pub task_id: String,
278    pub tool_use_id: String,
279    pub description: String,
280    pub last_tool_name: String,
281    pub usage: TaskUsage,
282    pub uuid: String,
283}
284
285/// `task_notification` system message — emitted once when a background
286/// task completes or fails.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct TaskNotificationMessage {
289    pub session_id: String,
290    pub task_id: String,
291    pub status: TaskStatus,
292    pub summary: String,
293    pub output_file: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub tool_use_id: Option<String>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub usage: Option<TaskUsage>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub uuid: Option<String>,
300}
301
302/// Assistant message
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct AssistantMessage {
305    pub message: AssistantMessageContent,
306    pub session_id: String,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub uuid: Option<String>,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub parent_tool_use_id: Option<String>,
311}
312
313/// Nested message content for assistant messages
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct AssistantMessageContent {
316    pub id: String,
317    pub role: String,
318    pub model: String,
319    pub content: Vec<ContentBlock>,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub stop_reason: Option<String>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub stop_sequence: Option<String>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub usage: Option<AssistantUsage>,
326}
327
328/// Usage information for assistant messages
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct AssistantUsage {
331    /// Number of input tokens
332    #[serde(default)]
333    pub input_tokens: u32,
334
335    /// Number of output tokens
336    #[serde(default)]
337    pub output_tokens: u32,
338
339    /// Tokens used to create cache
340    #[serde(default)]
341    pub cache_creation_input_tokens: u32,
342
343    /// Tokens read from cache
344    #[serde(default)]
345    pub cache_read_input_tokens: u32,
346
347    /// Service tier used (e.g., "standard")
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub service_tier: Option<String>,
350
351    /// Detailed cache creation breakdown
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub cache_creation: Option<CacheCreationDetails>,
354}
355
356/// Detailed cache creation information
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct CacheCreationDetails {
359    /// Ephemeral 1-hour input tokens
360    #[serde(default)]
361    pub ephemeral_1h_input_tokens: u32,
362
363    /// Ephemeral 5-minute input tokens
364    #[serde(default)]
365    pub ephemeral_5m_input_tokens: u32,
366}
367
368#[cfg(test)]
369mod tests {
370    use crate::io::ClaudeOutput;
371
372    #[test]
373    fn test_system_message_init() {
374        let json = r#"{
375            "type": "system",
376            "subtype": "init",
377            "session_id": "test-session-123",
378            "cwd": "/home/user/project",
379            "model": "claude-sonnet-4",
380            "tools": ["Bash", "Read", "Write"],
381            "mcp_servers": [],
382            "slash_commands": ["compact", "cost", "review"],
383            "agents": ["Bash", "Explore", "Plan"],
384            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
385            "skills": [],
386            "claude_code_version": "2.1.15",
387            "apiKeySource": "none",
388            "output_style": "default",
389            "permissionMode": "default"
390        }"#;
391
392        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
393        if let ClaudeOutput::System(sys) = output {
394            assert!(sys.is_init());
395            assert!(!sys.is_status());
396            assert!(!sys.is_compact_boundary());
397
398            let init = sys.as_init().expect("Should parse as init");
399            assert_eq!(init.session_id, "test-session-123");
400            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
401            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
402            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
403            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
404            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
405            assert_eq!(init.plugins.len(), 1);
406            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
407            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
408            assert_eq!(init.api_key_source, Some("none".to_string()));
409            assert_eq!(init.output_style, Some("default".to_string()));
410            assert_eq!(init.permission_mode, Some("default".to_string()));
411        } else {
412            panic!("Expected System message");
413        }
414    }
415
416    #[test]
417    fn test_system_message_init_from_real_capture() {
418        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
419        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
420        if let ClaudeOutput::System(sys) = output {
421            let init = sys.as_init().expect("Should parse real init capture");
422            assert_eq!(init.slash_commands.len(), 8);
423            assert!(init.slash_commands.contains(&"compact".to_string()));
424            assert!(init.slash_commands.contains(&"review".to_string()));
425            assert_eq!(init.agents.len(), 5);
426            assert!(init.agents.contains(&"Bash".to_string()));
427            assert!(init.agents.contains(&"Explore".to_string()));
428            assert_eq!(init.plugins.len(), 1);
429            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
430            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
431        } else {
432            panic!("Expected System message");
433        }
434    }
435
436    #[test]
437    fn test_system_message_status() {
438        let json = r#"{
439            "type": "system",
440            "subtype": "status",
441            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
442            "status": "compacting",
443            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
444        }"#;
445
446        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
447        if let ClaudeOutput::System(sys) = output {
448            assert!(sys.is_status());
449            assert!(!sys.is_init());
450
451            let status = sys.as_status().expect("Should parse as status");
452            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
453            assert_eq!(status.status, Some("compacting".to_string()));
454            assert_eq!(
455                status.uuid,
456                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
457            );
458        } else {
459            panic!("Expected System message");
460        }
461    }
462
463    #[test]
464    fn test_system_message_status_null() {
465        let json = r#"{
466            "type": "system",
467            "subtype": "status",
468            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
469            "status": null,
470            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
471        }"#;
472
473        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
474        if let ClaudeOutput::System(sys) = output {
475            let status = sys.as_status().expect("Should parse as status");
476            assert_eq!(status.status, None);
477        } else {
478            panic!("Expected System message");
479        }
480    }
481
482    #[test]
483    fn test_system_message_task_started() {
484        let json = r#"{
485            "type": "system",
486            "subtype": "task_started",
487            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
488            "task_id": "b6daf3f",
489            "task_type": "local_bash",
490            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
491            "description": "Wait for CI on PR #12",
492            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
493        }"#;
494
495        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
496        if let ClaudeOutput::System(sys) = output {
497            assert!(sys.is_task_started());
498            assert!(!sys.is_task_progress());
499            assert!(!sys.is_task_notification());
500
501            let task = sys.as_task_started().expect("Should parse as task_started");
502            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
503            assert_eq!(task.task_id, "b6daf3f");
504            assert_eq!(task.task_type, super::TaskType::LocalBash);
505            assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
506            assert_eq!(task.description, "Wait for CI on PR #12");
507        } else {
508            panic!("Expected System message");
509        }
510    }
511
512    #[test]
513    fn test_system_message_task_started_agent() {
514        let json = r#"{
515            "type": "system",
516            "subtype": "task_started",
517            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
518            "task_id": "a4a7e0906e5fc64cc",
519            "task_type": "local_agent",
520            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
521            "description": "Explore Scene/ArrayScene duplication",
522            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
523        }"#;
524
525        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
526        if let ClaudeOutput::System(sys) = output {
527            let task = sys.as_task_started().expect("Should parse as task_started");
528            assert_eq!(task.task_type, super::TaskType::LocalAgent);
529            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
530        } else {
531            panic!("Expected System message");
532        }
533    }
534
535    #[test]
536    fn test_system_message_task_progress() {
537        let json = r#"{
538            "type": "system",
539            "subtype": "task_progress",
540            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
541            "task_id": "a4a7e0906e5fc64cc",
542            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
543            "description": "Reading src/jplephem/chebyshev.rs",
544            "last_tool_name": "Read",
545            "usage": {
546                "duration_ms": 13996,
547                "tool_uses": 9,
548                "total_tokens": 38779
549            },
550            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
551        }"#;
552
553        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
554        if let ClaudeOutput::System(sys) = output {
555            assert!(sys.is_task_progress());
556            assert!(!sys.is_task_started());
557
558            let progress = sys
559                .as_task_progress()
560                .expect("Should parse as task_progress");
561            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
562            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
563            assert_eq!(progress.last_tool_name, "Read");
564            assert_eq!(progress.usage.duration_ms, 13996);
565            assert_eq!(progress.usage.tool_uses, 9);
566            assert_eq!(progress.usage.total_tokens, 38779);
567        } else {
568            panic!("Expected System message");
569        }
570    }
571
572    #[test]
573    fn test_system_message_task_notification_completed() {
574        let json = r#"{
575            "type": "system",
576            "subtype": "task_notification",
577            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
578            "task_id": "a0ba761e9dc9c316f",
579            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
580            "status": "completed",
581            "summary": "Agent \"Write Hipparcos data source doc\" completed",
582            "output_file": "",
583            "usage": {
584                "duration_ms": 172300,
585                "tool_uses": 11,
586                "total_tokens": 42005
587            },
588            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
589        }"#;
590
591        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
592        if let ClaudeOutput::System(sys) = output {
593            assert!(sys.is_task_notification());
594
595            let notif = sys
596                .as_task_notification()
597                .expect("Should parse as task_notification");
598            assert_eq!(notif.status, super::TaskStatus::Completed);
599            assert_eq!(
600                notif.summary,
601                "Agent \"Write Hipparcos data source doc\" completed"
602            );
603            assert_eq!(notif.output_file, Some("".to_string()));
604            assert_eq!(
605                notif.tool_use_id,
606                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
607            );
608            let usage = notif.usage.expect("Should have usage");
609            assert_eq!(usage.duration_ms, 172300);
610            assert_eq!(usage.tool_uses, 11);
611            assert_eq!(usage.total_tokens, 42005);
612        } else {
613            panic!("Expected System message");
614        }
615    }
616
617    #[test]
618    fn test_system_message_task_notification_failed_no_usage() {
619        let json = r#"{
620            "type": "system",
621            "subtype": "task_notification",
622            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
623            "task_id": "b98f6a3",
624            "status": "failed",
625            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
626            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
627        }"#;
628
629        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
630        if let ClaudeOutput::System(sys) = output {
631            let notif = sys
632                .as_task_notification()
633                .expect("Should parse as task_notification");
634            assert_eq!(notif.status, super::TaskStatus::Failed);
635            assert!(notif.tool_use_id.is_none());
636            assert!(notif.usage.is_none());
637            assert_eq!(
638                notif.output_file,
639                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
640            );
641        } else {
642            panic!("Expected System message");
643        }
644    }
645
646    #[test]
647    fn test_system_message_compact_boundary() {
648        let json = r#"{
649            "type": "system",
650            "subtype": "compact_boundary",
651            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
652            "compact_metadata": {
653                "pre_tokens": 155285,
654                "trigger": "auto"
655            },
656            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
657        }"#;
658
659        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
660        if let ClaudeOutput::System(sys) = output {
661            assert!(sys.is_compact_boundary());
662            assert!(!sys.is_init());
663            assert!(!sys.is_status());
664
665            let compact = sys
666                .as_compact_boundary()
667                .expect("Should parse as compact_boundary");
668            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
669            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
670            assert_eq!(compact.compact_metadata.trigger, "auto");
671        } else {
672            panic!("Expected System message");
673        }
674    }
675}