1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4use uuid::Uuid;
5
6use super::content_blocks::{deserialize_content_blocks, ContentBlock};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum SystemSubtype {
15 Init,
16 Status,
17 CompactBoundary,
18 ThinkingTokens,
19 TaskStarted,
20 TaskProgress,
21 TaskUpdated,
22 TaskNotification,
23 Unknown(String),
25}
26
27impl SystemSubtype {
28 pub fn as_str(&self) -> &str {
29 match self {
30 Self::Init => "init",
31 Self::Status => "status",
32 Self::CompactBoundary => "compact_boundary",
33 Self::ThinkingTokens => "thinking_tokens",
34 Self::TaskStarted => "task_started",
35 Self::TaskProgress => "task_progress",
36 Self::TaskUpdated => "task_updated",
37 Self::TaskNotification => "task_notification",
38 Self::Unknown(s) => s.as_str(),
39 }
40 }
41}
42
43impl fmt::Display for SystemSubtype {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str(self.as_str())
46 }
47}
48
49impl From<&str> for SystemSubtype {
50 fn from(s: &str) -> Self {
51 match s {
52 "init" => Self::Init,
53 "status" => Self::Status,
54 "compact_boundary" => Self::CompactBoundary,
55 "thinking_tokens" => Self::ThinkingTokens,
56 "task_started" => Self::TaskStarted,
57 "task_progress" => Self::TaskProgress,
58 "task_updated" => Self::TaskUpdated,
59 "task_notification" => Self::TaskNotification,
60 other => Self::Unknown(other.to_string()),
61 }
62 }
63}
64
65impl Serialize for SystemSubtype {
66 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
67 serializer.serialize_str(self.as_str())
68 }
69}
70
71impl<'de> Deserialize<'de> for SystemSubtype {
72 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
73 let s = String::deserialize(deserializer)?;
74 Ok(Self::from(s.as_str()))
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub enum MessageRole {
84 User,
85 Assistant,
86 Unknown(String),
88}
89
90impl MessageRole {
91 pub fn as_str(&self) -> &str {
92 match self {
93 Self::User => "user",
94 Self::Assistant => "assistant",
95 Self::Unknown(s) => s.as_str(),
96 }
97 }
98}
99
100impl fmt::Display for MessageRole {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(self.as_str())
103 }
104}
105
106impl From<&str> for MessageRole {
107 fn from(s: &str) -> Self {
108 match s {
109 "user" => Self::User,
110 "assistant" => Self::Assistant,
111 other => Self::Unknown(other.to_string()),
112 }
113 }
114}
115
116impl Serialize for MessageRole {
117 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
118 serializer.serialize_str(self.as_str())
119 }
120}
121
122impl<'de> Deserialize<'de> for MessageRole {
123 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
124 let s = String::deserialize(deserializer)?;
125 Ok(Self::from(s.as_str()))
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131pub enum CompactionTrigger {
132 Auto,
134 Manual,
136 Unknown(String),
138}
139
140impl CompactionTrigger {
141 pub fn as_str(&self) -> &str {
142 match self {
143 Self::Auto => "auto",
144 Self::Manual => "manual",
145 Self::Unknown(s) => s.as_str(),
146 }
147 }
148}
149
150impl fmt::Display for CompactionTrigger {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.write_str(self.as_str())
153 }
154}
155
156impl From<&str> for CompactionTrigger {
157 fn from(s: &str) -> Self {
158 match s {
159 "auto" => Self::Auto,
160 "manual" => Self::Manual,
161 other => Self::Unknown(other.to_string()),
162 }
163 }
164}
165
166impl Serialize for CompactionTrigger {
167 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
168 serializer.serialize_str(self.as_str())
169 }
170}
171
172impl<'de> Deserialize<'de> for CompactionTrigger {
173 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
174 let s = String::deserialize(deserializer)?;
175 Ok(Self::from(s.as_str()))
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Hash)]
181pub enum StopReason {
182 EndTurn,
184 MaxTokens,
186 ToolUse,
188 Unknown(String),
190}
191
192impl StopReason {
193 pub fn as_str(&self) -> &str {
194 match self {
195 Self::EndTurn => "end_turn",
196 Self::MaxTokens => "max_tokens",
197 Self::ToolUse => "tool_use",
198 Self::Unknown(s) => s.as_str(),
199 }
200 }
201}
202
203impl fmt::Display for StopReason {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 f.write_str(self.as_str())
206 }
207}
208
209impl From<&str> for StopReason {
210 fn from(s: &str) -> Self {
211 match s {
212 "end_turn" => Self::EndTurn,
213 "max_tokens" => Self::MaxTokens,
214 "tool_use" => Self::ToolUse,
215 other => Self::Unknown(other.to_string()),
216 }
217 }
218}
219
220impl Serialize for StopReason {
221 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
222 serializer.serialize_str(self.as_str())
223 }
224}
225
226impl<'de> Deserialize<'de> for StopReason {
227 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
228 let s = String::deserialize(deserializer)?;
229 Ok(Self::from(s.as_str()))
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Hash)]
235pub enum ApiKeySource {
236 None,
238 Unknown(String),
240}
241
242impl ApiKeySource {
243 pub fn as_str(&self) -> &str {
244 match self {
245 Self::None => "none",
246 Self::Unknown(s) => s.as_str(),
247 }
248 }
249}
250
251impl fmt::Display for ApiKeySource {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 f.write_str(self.as_str())
254 }
255}
256
257impl From<&str> for ApiKeySource {
258 fn from(s: &str) -> Self {
259 match s {
260 "none" => Self::None,
261 other => Self::Unknown(other.to_string()),
262 }
263 }
264}
265
266impl Serialize for ApiKeySource {
267 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
268 serializer.serialize_str(self.as_str())
269 }
270}
271
272impl<'de> Deserialize<'de> for ApiKeySource {
273 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
274 let s = String::deserialize(deserializer)?;
275 Ok(Self::from(s.as_str()))
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Hash)]
281pub enum OutputStyle {
282 Default,
284 Unknown(String),
286}
287
288impl OutputStyle {
289 pub fn as_str(&self) -> &str {
290 match self {
291 Self::Default => "default",
292 Self::Unknown(s) => s.as_str(),
293 }
294 }
295}
296
297impl fmt::Display for OutputStyle {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 f.write_str(self.as_str())
300 }
301}
302
303impl From<&str> for OutputStyle {
304 fn from(s: &str) -> Self {
305 match s {
306 "default" => Self::Default,
307 other => Self::Unknown(other.to_string()),
308 }
309 }
310}
311
312impl Serialize for OutputStyle {
313 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
314 serializer.serialize_str(self.as_str())
315 }
316}
317
318impl<'de> Deserialize<'de> for OutputStyle {
319 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
320 let s = String::deserialize(deserializer)?;
321 Ok(Self::from(s.as_str()))
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Hash)]
327pub enum InitPermissionMode {
328 Default,
330 Unknown(String),
332}
333
334impl InitPermissionMode {
335 pub fn as_str(&self) -> &str {
336 match self {
337 Self::Default => "default",
338 Self::Unknown(s) => s.as_str(),
339 }
340 }
341}
342
343impl fmt::Display for InitPermissionMode {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 f.write_str(self.as_str())
346 }
347}
348
349impl From<&str> for InitPermissionMode {
350 fn from(s: &str) -> Self {
351 match s {
352 "default" => Self::Default,
353 other => Self::Unknown(other.to_string()),
354 }
355 }
356}
357
358impl Serialize for InitPermissionMode {
359 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
360 serializer.serialize_str(self.as_str())
361 }
362}
363
364impl<'de> Deserialize<'de> for InitPermissionMode {
365 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
366 let s = String::deserialize(deserializer)?;
367 Ok(Self::from(s.as_str()))
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Hash)]
373pub enum StatusMessageStatus {
374 Compacting,
376 Unknown(String),
378}
379
380impl StatusMessageStatus {
381 pub fn as_str(&self) -> &str {
382 match self {
383 Self::Compacting => "compacting",
384 Self::Unknown(s) => s.as_str(),
385 }
386 }
387}
388
389impl fmt::Display for StatusMessageStatus {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 f.write_str(self.as_str())
392 }
393}
394
395impl From<&str> for StatusMessageStatus {
396 fn from(s: &str) -> Self {
397 match s {
398 "compacting" => Self::Compacting,
399 other => Self::Unknown(other.to_string()),
400 }
401 }
402}
403
404impl Serialize for StatusMessageStatus {
405 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
406 serializer.serialize_str(self.as_str())
407 }
408}
409
410impl<'de> Deserialize<'de> for StatusMessageStatus {
411 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
412 let s = String::deserialize(deserializer)?;
413 Ok(Self::from(s.as_str()))
414 }
415}
416
417pub(crate) fn serialize_optional_uuid<S>(
419 uuid: &Option<Uuid>,
420 serializer: S,
421) -> Result<S::Ok, S::Error>
422where
423 S: Serializer,
424{
425 match uuid {
426 Some(id) => serializer.serialize_str(&id.to_string()),
427 None => serializer.serialize_none(),
428 }
429}
430
431pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
433where
434 D: Deserializer<'de>,
435{
436 let opt_str: Option<String> = Option::deserialize(deserializer)?;
437 match opt_str {
438 Some(s) => Uuid::parse_str(&s)
439 .map(Some)
440 .map_err(serde::de::Error::custom),
441 None => Ok(None),
442 }
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct UserMessage {
448 pub message: MessageContent,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 #[serde(
451 serialize_with = "serialize_optional_uuid",
452 deserialize_with = "deserialize_optional_uuid"
453 )]
454 pub session_id: Option<Uuid>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub parent_tool_use_id: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub uuid: Option<String>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub timestamp: Option<String>,
464 #[serde(skip_serializing_if = "Option::is_none")]
471 pub tool_use_result: Option<serde_json::Value>,
472 #[serde(skip_serializing_if = "Option::is_none")]
475 pub subagent_type: Option<String>,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub task_description: Option<String>,
479}
480
481impl UserMessage {
482 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
490 &self,
491 ) -> Option<Result<T, serde_json::Error>> {
492 self.tool_use_result
493 .as_ref()
494 .map(|v| serde_json::from_value(v.clone()))
495 }
496
497 pub fn subagent_result(&self) -> Option<SubagentResult> {
508 self.tool_use_result
509 .as_ref()
510 .and_then(|v| serde_json::from_value(v.clone()).ok())
511 }
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct SubagentResult {
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub status: Option<String>,
529 #[serde(skip_serializing_if = "Option::is_none")]
531 pub prompt: Option<String>,
532 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
534 pub agent_id: Option<String>,
535 #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
537 pub agent_type: Option<String>,
538 #[serde(
540 default,
541 deserialize_with = "deserialize_content_blocks",
542 skip_serializing_if = "Vec::is_empty"
543 )]
544 pub content: Vec<ContentBlock>,
545 #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
547 pub resolved_model: Option<String>,
548 #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
550 pub total_duration_ms: Option<u64>,
551 #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
553 pub total_tokens: Option<u64>,
554 #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
556 pub total_tool_use_count: Option<u64>,
557 #[serde(skip_serializing_if = "Option::is_none")]
559 pub usage: Option<super::result::UsageInfo>,
560 #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
562 pub tool_stats: Option<SubagentToolStats>,
563}
564
565#[derive(Debug, Clone, Default, Serialize, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct SubagentToolStats {
572 #[serde(default)]
573 pub read_count: u64,
574 #[serde(default)]
575 pub search_count: u64,
576 #[serde(default)]
577 pub bash_count: u64,
578 #[serde(default)]
579 pub edit_file_count: u64,
580 #[serde(default)]
581 pub lines_added: u64,
582 #[serde(default)]
583 pub lines_removed: u64,
584 #[serde(default)]
585 pub other_tool_count: u64,
586 #[serde(flatten)]
587 pub extra: serde_json::Map<String, Value>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct MessageContent {
593 pub role: MessageRole,
594 #[serde(deserialize_with = "deserialize_content_blocks")]
595 pub content: Vec<ContentBlock>,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct SystemMessage {
601 pub subtype: SystemSubtype,
602 #[serde(flatten)]
603 pub data: Value, }
605
606impl SystemMessage {
607 pub fn is_init(&self) -> bool {
609 self.subtype == SystemSubtype::Init
610 }
611
612 pub fn is_status(&self) -> bool {
614 self.subtype == SystemSubtype::Status
615 }
616
617 pub fn is_compact_boundary(&self) -> bool {
619 self.subtype == SystemSubtype::CompactBoundary
620 }
621
622 pub fn as_init(&self) -> Option<InitMessage> {
624 if self.subtype != SystemSubtype::Init {
625 return None;
626 }
627 serde_json::from_value(self.data.clone()).ok()
628 }
629
630 pub fn as_status(&self) -> Option<StatusMessage> {
632 if self.subtype != SystemSubtype::Status {
633 return None;
634 }
635 serde_json::from_value(self.data.clone()).ok()
636 }
637
638 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
640 if self.subtype != SystemSubtype::CompactBoundary {
641 return None;
642 }
643 serde_json::from_value(self.data.clone()).ok()
644 }
645
646 pub fn is_task_started(&self) -> bool {
648 self.subtype == SystemSubtype::TaskStarted
649 }
650
651 pub fn is_task_progress(&self) -> bool {
653 self.subtype == SystemSubtype::TaskProgress
654 }
655
656 pub fn is_task_notification(&self) -> bool {
658 self.subtype == SystemSubtype::TaskNotification
659 }
660
661 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
663 if self.subtype != SystemSubtype::TaskStarted {
664 return None;
665 }
666 serde_json::from_value(self.data.clone()).ok()
667 }
668
669 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
671 if self.subtype != SystemSubtype::TaskProgress {
672 return None;
673 }
674 serde_json::from_value(self.data.clone()).ok()
675 }
676
677 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
679 if self.subtype != SystemSubtype::TaskNotification {
680 return None;
681 }
682 serde_json::from_value(self.data.clone()).ok()
683 }
684
685 pub fn is_task_updated(&self) -> bool {
687 self.subtype == SystemSubtype::TaskUpdated
688 }
689
690 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
692 if self.subtype != SystemSubtype::TaskUpdated {
693 return None;
694 }
695 serde_json::from_value(self.data.clone()).ok()
696 }
697
698 pub fn is_thinking_tokens(&self) -> bool {
700 self.subtype == SystemSubtype::ThinkingTokens
701 }
702
703 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
705 if self.subtype != SystemSubtype::ThinkingTokens {
706 return None;
707 }
708 serde_json::from_value(self.data.clone()).ok()
709 }
710
711 pub fn typed_value(&self) -> Option<Value> {
720 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
721 parsed.and_then(|v| serde_json::to_value(v).ok())
722 }
723 match self.subtype {
724 SystemSubtype::Init => reserialize(self.as_init()),
725 SystemSubtype::Status => reserialize(self.as_status()),
726 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
727 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
728 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
729 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
730 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
731 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
732 SystemSubtype::Unknown(_) => None,
733 }
734 }
735}
736
737#[derive(Debug, Clone, Serialize, Deserialize)]
739pub struct PluginInfo {
740 pub name: String,
742 pub path: String,
744 #[serde(skip_serializing_if = "Option::is_none")]
746 pub source: Option<String>,
747}
748
749#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct InitMessage {
752 pub session_id: String,
754 #[serde(skip_serializing_if = "Option::is_none")]
756 pub cwd: Option<String>,
757 #[serde(skip_serializing_if = "Option::is_none")]
759 pub model: Option<String>,
760 #[serde(default, skip_serializing_if = "Vec::is_empty")]
762 pub tools: Vec<String>,
763 #[serde(default, skip_serializing_if = "Vec::is_empty")]
765 pub mcp_servers: Vec<Value>,
766 #[serde(default, skip_serializing_if = "Vec::is_empty")]
768 pub slash_commands: Vec<String>,
769 #[serde(default, skip_serializing_if = "Vec::is_empty")]
771 pub agents: Vec<String>,
772 #[serde(default, skip_serializing_if = "Vec::is_empty")]
774 pub plugins: Vec<PluginInfo>,
775 #[serde(default, skip_serializing_if = "Vec::is_empty")]
777 pub skills: Vec<Value>,
778 #[serde(skip_serializing_if = "Option::is_none")]
780 pub claude_code_version: Option<String>,
781 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
783 pub api_key_source: Option<ApiKeySource>,
784 #[serde(skip_serializing_if = "Option::is_none")]
786 pub output_style: Option<OutputStyle>,
787 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
789 pub permission_mode: Option<InitPermissionMode>,
790
791 #[serde(skip_serializing_if = "Option::is_none")]
793 pub uuid: Option<String>,
794
795 #[serde(skip_serializing_if = "Option::is_none")]
797 pub memory_paths: Option<Value>,
798
799 #[serde(skip_serializing_if = "Option::is_none")]
801 pub fast_mode_state: Option<String>,
802
803 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub analytics_disabled: Option<bool>,
806
807 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub product_feedback_disabled: Option<bool>,
810}
811
812#[derive(Debug, Clone, Serialize, Deserialize)]
814pub struct StatusMessage {
815 pub session_id: String,
817 pub status: Option<StatusMessageStatus>,
819 #[serde(skip_serializing_if = "Option::is_none")]
821 pub uuid: Option<String>,
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize)]
826pub struct CompactBoundaryMessage {
827 pub session_id: String,
829 pub compact_metadata: CompactMetadata,
831 #[serde(
835 default,
836 skip_serializing_if = "Option::is_none",
837 alias = "content",
838 alias = "text"
839 )]
840 pub summary: Option<String>,
841 #[serde(
845 default,
846 skip_serializing_if = "Option::is_none",
847 alias = "message_count"
848 )]
849 pub leaf_message_count: Option<u32>,
850 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub duration_ms: Option<u64>,
853 #[serde(skip_serializing_if = "Option::is_none")]
855 pub uuid: Option<String>,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct CompactMetadata {
861 pub pre_tokens: u64,
863 pub trigger: CompactionTrigger,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
873pub struct TaskUsage {
874 pub duration_ms: u64,
876 pub tool_uses: u64,
878 pub total_tokens: u64,
880}
881
882#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
884#[serde(rename_all = "snake_case")]
885pub enum TaskType {
886 LocalAgent,
888 LocalBash,
890}
891
892#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
894#[serde(rename_all = "snake_case")]
895pub enum TaskStatus {
896 Completed,
897 Failed,
898}
899
900#[derive(Debug, Clone, Serialize, Deserialize)]
902pub struct TaskStartedMessage {
903 pub session_id: String,
904 pub task_id: String,
905 pub task_type: TaskType,
906 pub tool_use_id: String,
907 pub description: String,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
911 pub subagent_type: Option<String>,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
914 pub prompt: Option<String>,
915 pub uuid: String,
916}
917
918#[derive(Debug, Clone, Serialize, Deserialize)]
922pub struct TaskUpdatedMessage {
923 pub session_id: String,
924 pub task_id: String,
925 pub patch: TaskPatch,
926 pub uuid: String,
927}
928
929#[derive(Debug, Clone, Default, Serialize, Deserialize)]
932pub struct TaskPatch {
933 #[serde(default, skip_serializing_if = "Option::is_none")]
934 pub status: Option<TaskStatus>,
935 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub end_time: Option<u64>,
939}
940
941#[derive(Debug, Clone, Serialize, Deserialize)]
944pub struct ThinkingTokensMessage {
945 pub session_id: String,
946 pub estimated_tokens: u64,
948 pub estimated_tokens_delta: u64,
950 pub uuid: String,
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize)]
956pub struct TaskProgressMessage {
957 pub session_id: String,
958 pub task_id: String,
959 pub tool_use_id: String,
960 pub description: String,
961 pub last_tool_name: String,
962 pub usage: TaskUsage,
963 #[serde(default, skip_serializing_if = "Option::is_none")]
965 pub subagent_type: Option<String>,
966 pub uuid: String,
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize)]
972pub struct TaskNotificationMessage {
973 pub session_id: String,
974 pub task_id: String,
975 pub status: TaskStatus,
976 pub summary: String,
977 pub output_file: Option<String>,
978 #[serde(skip_serializing_if = "Option::is_none")]
979 pub tool_use_id: Option<String>,
980 #[serde(skip_serializing_if = "Option::is_none")]
981 pub usage: Option<TaskUsage>,
982 #[serde(skip_serializing_if = "Option::is_none")]
983 pub uuid: Option<String>,
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct AssistantMessage {
989 pub message: AssistantMessageContent,
990 pub session_id: String,
991 #[serde(skip_serializing_if = "Option::is_none")]
992 pub uuid: Option<String>,
993 #[serde(skip_serializing_if = "Option::is_none")]
994 pub parent_tool_use_id: Option<String>,
995 #[serde(skip_serializing_if = "Option::is_none")]
997 pub request_id: Option<String>,
998 #[serde(skip_serializing_if = "Option::is_none")]
1001 pub subagent_type: Option<String>,
1002 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub task_description: Option<String>,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub struct AssistantMessageContent {
1010 pub id: String,
1011 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1013 pub message_type: Option<String>,
1014 pub role: MessageRole,
1015 pub model: String,
1016 pub content: Vec<ContentBlock>,
1017 #[serde(skip_serializing_if = "Option::is_none")]
1018 pub stop_reason: Option<StopReason>,
1019 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub stop_sequence: Option<String>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub usage: Option<AssistantUsage>,
1023 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub stop_details: Option<Value>,
1026 #[serde(skip_serializing_if = "Option::is_none")]
1028 pub context_management: Option<Value>,
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1033pub struct AssistantUsage {
1034 #[serde(default)]
1036 pub input_tokens: u32,
1037
1038 #[serde(default)]
1040 pub output_tokens: u32,
1041
1042 #[serde(default)]
1044 pub cache_creation_input_tokens: u32,
1045
1046 #[serde(default)]
1048 pub cache_read_input_tokens: u32,
1049
1050 #[serde(skip_serializing_if = "Option::is_none")]
1052 pub service_tier: Option<String>,
1053
1054 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub cache_creation: Option<CacheCreationDetails>,
1057
1058 #[serde(skip_serializing_if = "Option::is_none")]
1060 pub inference_geo: Option<String>,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1065pub struct CacheCreationDetails {
1066 #[serde(default)]
1068 pub ephemeral_1h_input_tokens: u32,
1069
1070 #[serde(default)]
1072 pub ephemeral_5m_input_tokens: u32,
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 use crate::io::ClaudeOutput;
1078
1079 #[test]
1080 fn test_system_message_init() {
1081 let json = r#"{
1082 "type": "system",
1083 "subtype": "init",
1084 "session_id": "test-session-123",
1085 "cwd": "/home/user/project",
1086 "model": "claude-sonnet-4",
1087 "tools": ["Bash", "Read", "Write"],
1088 "mcp_servers": [],
1089 "slash_commands": ["compact", "cost", "review"],
1090 "agents": ["Bash", "Explore", "Plan"],
1091 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
1092 "skills": [],
1093 "claude_code_version": "2.1.15",
1094 "apiKeySource": "none",
1095 "output_style": "default",
1096 "permissionMode": "default"
1097 }"#;
1098
1099 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1100 if let ClaudeOutput::System(sys) = output {
1101 assert!(sys.is_init());
1102 assert!(!sys.is_status());
1103 assert!(!sys.is_compact_boundary());
1104
1105 let init = sys.as_init().expect("Should parse as init");
1106 assert_eq!(init.session_id, "test-session-123");
1107 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
1108 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
1109 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
1110 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
1111 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
1112 assert_eq!(init.plugins.len(), 1);
1113 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
1114 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
1115 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
1116 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
1117 assert_eq!(
1118 init.permission_mode,
1119 Some(super::InitPermissionMode::Default)
1120 );
1121 } else {
1122 panic!("Expected System message");
1123 }
1124 }
1125
1126 #[test]
1127 fn test_system_message_init_from_real_capture() {
1128 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
1129 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1130 if let ClaudeOutput::System(sys) = output {
1131 let init = sys.as_init().expect("Should parse real init capture");
1132 assert_eq!(init.slash_commands.len(), 8);
1133 assert!(init.slash_commands.contains(&"compact".to_string()));
1134 assert!(init.slash_commands.contains(&"review".to_string()));
1135 assert_eq!(init.agents.len(), 5);
1136 assert!(init.agents.contains(&"Bash".to_string()));
1137 assert!(init.agents.contains(&"Explore".to_string()));
1138 assert_eq!(init.plugins.len(), 1);
1139 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
1140 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
1141 } else {
1142 panic!("Expected System message");
1143 }
1144 }
1145
1146 #[test]
1147 fn test_system_message_status() {
1148 let json = r#"{
1149 "type": "system",
1150 "subtype": "status",
1151 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1152 "status": "compacting",
1153 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
1154 }"#;
1155
1156 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1157 if let ClaudeOutput::System(sys) = output {
1158 assert!(sys.is_status());
1159 assert!(!sys.is_init());
1160
1161 let status = sys.as_status().expect("Should parse as status");
1162 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1163 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
1164 assert_eq!(
1165 status.uuid,
1166 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
1167 );
1168 } else {
1169 panic!("Expected System message");
1170 }
1171 }
1172
1173 #[test]
1174 fn test_system_message_status_null() {
1175 let json = r#"{
1176 "type": "system",
1177 "subtype": "status",
1178 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1179 "status": null,
1180 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
1181 }"#;
1182
1183 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1184 if let ClaudeOutput::System(sys) = output {
1185 let status = sys.as_status().expect("Should parse as status");
1186 assert_eq!(status.status, None);
1187 } else {
1188 panic!("Expected System message");
1189 }
1190 }
1191
1192 #[test]
1193 fn test_system_message_task_started() {
1194 let json = r#"{
1195 "type": "system",
1196 "subtype": "task_started",
1197 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
1198 "task_id": "b6daf3f",
1199 "task_type": "local_bash",
1200 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
1201 "description": "Wait for CI on PR #12",
1202 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
1203 }"#;
1204
1205 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1206 if let ClaudeOutput::System(sys) = output {
1207 assert!(sys.is_task_started());
1208 assert!(!sys.is_task_progress());
1209 assert!(!sys.is_task_notification());
1210
1211 let task = sys.as_task_started().expect("Should parse as task_started");
1212 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
1213 assert_eq!(task.task_id, "b6daf3f");
1214 assert_eq!(task.task_type, super::TaskType::LocalBash);
1215 assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
1216 assert_eq!(task.description, "Wait for CI on PR #12");
1217 } else {
1218 panic!("Expected System message");
1219 }
1220 }
1221
1222 #[test]
1223 fn test_system_message_task_started_agent() {
1224 let json = r#"{
1225 "type": "system",
1226 "subtype": "task_started",
1227 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1228 "task_id": "a4a7e0906e5fc64cc",
1229 "task_type": "local_agent",
1230 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
1231 "description": "Explore Scene/ArrayScene duplication",
1232 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
1233 }"#;
1234
1235 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1236 if let ClaudeOutput::System(sys) = output {
1237 let task = sys.as_task_started().expect("Should parse as task_started");
1238 assert_eq!(task.task_type, super::TaskType::LocalAgent);
1239 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
1240 } else {
1241 panic!("Expected System message");
1242 }
1243 }
1244
1245 #[test]
1246 fn test_system_message_task_progress() {
1247 let json = r#"{
1248 "type": "system",
1249 "subtype": "task_progress",
1250 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1251 "task_id": "a4a7e0906e5fc64cc",
1252 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
1253 "description": "Reading src/jplephem/chebyshev.rs",
1254 "last_tool_name": "Read",
1255 "usage": {
1256 "duration_ms": 13996,
1257 "tool_uses": 9,
1258 "total_tokens": 38779
1259 },
1260 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
1261 }"#;
1262
1263 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1264 if let ClaudeOutput::System(sys) = output {
1265 assert!(sys.is_task_progress());
1266 assert!(!sys.is_task_started());
1267
1268 let progress = sys
1269 .as_task_progress()
1270 .expect("Should parse as task_progress");
1271 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
1272 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
1273 assert_eq!(progress.last_tool_name, "Read");
1274 assert_eq!(progress.usage.duration_ms, 13996);
1275 assert_eq!(progress.usage.tool_uses, 9);
1276 assert_eq!(progress.usage.total_tokens, 38779);
1277 } else {
1278 panic!("Expected System message");
1279 }
1280 }
1281
1282 #[test]
1283 fn test_system_message_task_notification_completed() {
1284 let json = r#"{
1285 "type": "system",
1286 "subtype": "task_notification",
1287 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1288 "task_id": "a0ba761e9dc9c316f",
1289 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
1290 "status": "completed",
1291 "summary": "Agent \"Write Hipparcos data source doc\" completed",
1292 "output_file": "",
1293 "usage": {
1294 "duration_ms": 172300,
1295 "tool_uses": 11,
1296 "total_tokens": 42005
1297 },
1298 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
1299 }"#;
1300
1301 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1302 if let ClaudeOutput::System(sys) = output {
1303 assert!(sys.is_task_notification());
1304
1305 let notif = sys
1306 .as_task_notification()
1307 .expect("Should parse as task_notification");
1308 assert_eq!(notif.status, super::TaskStatus::Completed);
1309 assert_eq!(
1310 notif.summary,
1311 "Agent \"Write Hipparcos data source doc\" completed"
1312 );
1313 assert_eq!(notif.output_file, Some("".to_string()));
1314 assert_eq!(
1315 notif.tool_use_id,
1316 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
1317 );
1318 let usage = notif.usage.expect("Should have usage");
1319 assert_eq!(usage.duration_ms, 172300);
1320 assert_eq!(usage.tool_uses, 11);
1321 assert_eq!(usage.total_tokens, 42005);
1322 } else {
1323 panic!("Expected System message");
1324 }
1325 }
1326
1327 #[test]
1328 fn test_system_message_task_notification_failed_no_usage() {
1329 let json = r#"{
1330 "type": "system",
1331 "subtype": "task_notification",
1332 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
1333 "task_id": "b98f6a3",
1334 "status": "failed",
1335 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
1336 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
1337 }"#;
1338
1339 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1340 if let ClaudeOutput::System(sys) = output {
1341 let notif = sys
1342 .as_task_notification()
1343 .expect("Should parse as task_notification");
1344 assert_eq!(notif.status, super::TaskStatus::Failed);
1345 assert!(notif.tool_use_id.is_none());
1346 assert!(notif.usage.is_none());
1347 assert_eq!(
1348 notif.output_file,
1349 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
1350 );
1351 } else {
1352 panic!("Expected System message");
1353 }
1354 }
1355
1356 #[test]
1362 fn test_task_messages_roundtrip_through_value() {
1363 let cases = [
1364 r#"{"type":"system","subtype":"task_started","session_id":"s1",
1365 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
1366 "description":"Sleep 3s","uuid":"u1"}"#,
1367 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
1368 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
1369 "last_tool_name":"Bash",
1370 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
1371 "uuid":"u2"}"#,
1372 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
1373 "task_id":"t1","tool_use_id":"tu1","status":"completed",
1374 "summary":"done","output_file":"",
1375 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
1376 "uuid":"u3"}"#,
1377 ];
1378
1379 for json in cases {
1380 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1381 let value = serde_json::to_value(&output).unwrap();
1382 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
1383
1384 let ClaudeOutput::System(sys) = reparsed else {
1385 panic!("Expected System variant after round-trip");
1386 };
1387
1388 match sys.subtype {
1389 super::SystemSubtype::TaskStarted => {
1390 assert!(
1391 sys.as_task_started().is_some(),
1392 "as_task_started failed after round-trip"
1393 );
1394 }
1395 super::SystemSubtype::TaskProgress => {
1396 assert!(
1397 sys.as_task_progress().is_some(),
1398 "as_task_progress failed after round-trip"
1399 );
1400 }
1401 super::SystemSubtype::TaskNotification => {
1402 assert!(
1403 sys.as_task_notification().is_some(),
1404 "as_task_notification failed after round-trip"
1405 );
1406 }
1407 other => panic!("unexpected subtype after round-trip: {other:?}"),
1408 }
1409 }
1410 }
1411
1412 #[test]
1413 fn test_system_message_compact_boundary() {
1414 let json = r#"{
1415 "type": "system",
1416 "subtype": "compact_boundary",
1417 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1418 "compact_metadata": {
1419 "pre_tokens": 155285,
1420 "trigger": "auto"
1421 },
1422 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1423 }"#;
1424
1425 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1426 if let ClaudeOutput::System(sys) = output {
1427 assert!(sys.is_compact_boundary());
1428 assert!(!sys.is_init());
1429 assert!(!sys.is_status());
1430
1431 let compact = sys
1432 .as_compact_boundary()
1433 .expect("Should parse as compact_boundary");
1434 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1435 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1436 assert_eq!(
1437 compact.compact_metadata.trigger,
1438 super::CompactionTrigger::Auto
1439 );
1440 assert!(compact.summary.is_none());
1442 assert!(compact.leaf_message_count.is_none());
1443 assert!(compact.duration_ms.is_none());
1444 } else {
1445 panic!("Expected System message");
1446 }
1447 }
1448
1449 #[test]
1450 fn test_compact_boundary_with_summary_stats() {
1451 let json = r#"{
1453 "type": "system",
1454 "subtype": "compact_boundary",
1455 "session_id": "s1",
1456 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
1457 "summary": "Summarized the earlier exploration.",
1458 "leaf_message_count": 42,
1459 "duration_ms": 1234,
1460 "uuid": "u1"
1461 }"#;
1462 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1463 let ClaudeOutput::System(sys) = output else {
1464 panic!("Expected System message");
1465 };
1466 let compact = sys.as_compact_boundary().expect("compact_boundary");
1467 assert_eq!(
1468 compact.summary.as_deref(),
1469 Some("Summarized the earlier exploration.")
1470 );
1471 assert_eq!(compact.leaf_message_count, Some(42));
1472 assert_eq!(compact.duration_ms, Some(1234));
1473
1474 let json_alt = r#"{
1477 "type": "system",
1478 "subtype": "compact_boundary",
1479 "session_id": "s2",
1480 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
1481 "content": "alt-key summary",
1482 "message_count": 7
1483 }"#;
1484 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
1485 let ClaudeOutput::System(sys) = output else {
1486 panic!("Expected System message");
1487 };
1488 let compact = sys.as_compact_boundary().expect("compact_boundary");
1489 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
1490 assert_eq!(compact.leaf_message_count, Some(7));
1491 }
1492
1493 #[test]
1494 fn test_init_message_with_new_fields() {
1495 let json = r#"{
1496 "type": "system",
1497 "subtype": "init",
1498 "session_id": "test-session",
1499 "cwd": "/home/user",
1500 "model": "claude-opus-4-7",
1501 "tools": ["Bash"],
1502 "mcp_servers": [],
1503 "permissionMode": "default",
1504 "apiKeySource": "none",
1505 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
1506 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
1507 "fast_mode_state": "off",
1508 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
1509 "claude_code_version": "2.1.117"
1510 }"#;
1511
1512 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1513 if let ClaudeOutput::System(sys) = output {
1514 let init = sys.as_init().expect("Should parse as init");
1515 assert_eq!(
1516 init.uuid.as_deref(),
1517 Some("44841a0d-182d-493a-86b5-79800d3d9665")
1518 );
1519 assert!(init.memory_paths.is_some());
1520 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
1521 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
1522 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
1523 } else {
1524 panic!("Expected System message");
1525 }
1526 }
1527
1528 #[test]
1529 fn test_assistant_message_with_new_fields() {
1530 let json = r#"{
1531 "type": "assistant",
1532 "message": {
1533 "id": "msg_1",
1534 "type": "message",
1535 "role": "assistant",
1536 "model": "claude-opus-4-7",
1537 "content": [{"type": "text", "text": "Hello"}],
1538 "stop_reason": "end_turn",
1539 "stop_details": null,
1540 "context_management": null,
1541 "usage": {
1542 "input_tokens": 100,
1543 "output_tokens": 10,
1544 "cache_creation_input_tokens": 50,
1545 "cache_read_input_tokens": 0,
1546 "service_tier": "standard",
1547 "inference_geo": "not_available"
1548 }
1549 },
1550 "session_id": "abc",
1551 "uuid": "msg-uuid-123"
1552 }"#;
1553
1554 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1555 if let ClaudeOutput::Assistant(asst) = output {
1556 assert_eq!(asst.message.stop_details, None);
1557 assert_eq!(asst.message.context_management, None);
1558 let usage = asst.message.usage.unwrap();
1559 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1560 } else {
1561 panic!("Expected Assistant message");
1562 }
1563 }
1564
1565 #[test]
1566 fn test_user_message_with_new_fields() {
1567 let json = r#"{
1568 "type": "user",
1569 "message": {
1570 "role": "user",
1571 "content": [{"type": "text", "text": "Hello"}]
1572 },
1573 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
1574 "parent_tool_use_id": "toolu_123",
1575 "uuid": "user-msg-456"
1576 }"#;
1577
1578 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1579 if let ClaudeOutput::User(user) = output {
1580 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
1581 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
1582 } else {
1583 panic!("Expected User message");
1584 }
1585 }
1586
1587 #[test]
1593 fn test_user_message_preserves_tool_use_result_and_timestamp() {
1594 let json = r#"{
1595 "type":"user",
1596 "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"}]},
1597 "parent_tool_use_id":null,
1598 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
1599 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
1600 "timestamp":"2026-05-12T23:12:04.121Z",
1601 "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"}}
1602 }"#;
1603
1604 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1605 let user = match output {
1606 ClaudeOutput::User(u) => u,
1607 other => panic!("Expected User message, got {:?}", other.message_type()),
1608 };
1609
1610 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
1611 let raw = user
1612 .tool_use_result
1613 .as_ref()
1614 .expect("tool_use_result must be captured");
1615 assert_eq!(raw["answers"]["Color"], "Blue");
1616 assert_eq!(raw["questions"][0]["header"], "Color");
1617
1618 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
1622 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
1623 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
1624 assert_eq!(
1625 reser["tool_use_result"]["questions"][0]["question"],
1626 "Which color do you prefer?"
1627 );
1628
1629 let typed: crate::AskUserQuestionInput = user
1632 .tool_use_result_as::<crate::AskUserQuestionInput>()
1633 .expect("tool_use_result present")
1634 .expect("AskUserQuestionInput parses");
1635 assert_eq!(typed.questions.len(), 1);
1636 assert_eq!(typed.questions[0].header, "Color");
1637 let answers = typed.answers.expect("answers populated");
1638 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
1639 }
1640
1641 #[test]
1644 fn test_user_message_without_tool_use_result_omits_field() {
1645 let json = r#"{
1646 "type":"user",
1647 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
1648 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
1649 }"#;
1650
1651 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1652 let user = match output {
1653 ClaudeOutput::User(u) => u,
1654 _ => panic!("Expected User message"),
1655 };
1656 assert!(user.tool_use_result.is_none());
1657 assert!(user.timestamp.is_none());
1658
1659 let reser = serde_json::to_value(&user).unwrap();
1660 assert!(reser.get("tool_use_result").is_none());
1661 assert!(reser.get("timestamp").is_none());
1662 }
1663
1664 #[test]
1668 fn test_subagent_result_exposes_token_accounting() {
1669 let json = r#"{
1670 "type":"user",
1671 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
1672 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
1673 "tool_use_result":{
1674 "status":"completed",
1675 "prompt":"Count the .rs files.",
1676 "agentId":"ac4f0276e9d4b6232",
1677 "agentType":"Explore",
1678 "content":[{"type":"text","text":"21"}],
1679 "resolvedModel":"claude-haiku-4-5-20251001",
1680 "totalDurationMs":6869,
1681 "totalTokens":7834,
1682 "totalToolUseCount":1,
1683 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
1684 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
1685 }
1686 }"#;
1687
1688 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1689 let user = match output {
1690 ClaudeOutput::User(u) => u,
1691 _ => panic!("Expected User message"),
1692 };
1693
1694 let result = user.subagent_result().expect("subagent result parses");
1695 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
1696 assert_eq!(
1697 result.resolved_model.as_deref(),
1698 Some("claude-haiku-4-5-20251001")
1699 );
1700 assert_eq!(result.total_tokens, Some(7834));
1701 assert_eq!(result.total_duration_ms, Some(6869));
1702 assert_eq!(result.total_tool_use_count, Some(1));
1703
1704 let usage = result.usage.expect("nested usage present");
1705 assert_eq!(usage.input_tokens, 6);
1706 assert_eq!(usage.cache_read_input_tokens, 7699);
1707
1708 let stats = result.tool_stats.expect("toolStats present");
1709 assert_eq!(stats.bash_count, 1);
1710 }
1711
1712 #[test]
1716 fn test_subagent_result_absent_for_non_task_result() {
1717 let json = r#"{
1718 "type":"user",
1719 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
1720 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
1721 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
1722 }"#;
1723
1724 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1725 let user = match output {
1726 ClaudeOutput::User(u) => u,
1727 _ => panic!("Expected User message"),
1728 };
1729
1730 let result = user.subagent_result().expect("lenient parse");
1731 assert_eq!(result.total_tokens, None);
1732 assert_eq!(result.agent_type, None);
1733 }
1734}