1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::{Map, Value};
3
4use super::content_blocks::{ContentBlock, ToolUseBlock};
5use super::control::{ControlCancelRequest, ControlRequest, ControlResponse};
6use super::errors::{AnthropicError, ParseError};
7use super::message_types::{AssistantMessage, SystemMessage, UserMessage};
8use super::rate_limit::RateLimitEvent;
9use super::result::ResultMessage;
10
11#[allow(clippy::large_enum_variant)]
13#[derive(Debug, Clone, Serialize)]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum ClaudeOutput {
16 System(SystemMessage),
18
19 User(UserMessage),
21
22 Assistant(AssistantMessage),
24
25 Result(ResultMessage),
27
28 #[serde(rename = "result")]
30 TranscriptResult(TranscriptMessage),
31
32 ControlRequest(ControlRequest),
34
35 ControlResponse(ControlResponse),
37
38 ControlCancelRequest(ControlCancelRequest),
41
42 Error(AnthropicError),
44
45 RateLimitEvent(RateLimitEvent),
47
48 StreamEvent(StreamEventMessage),
50
51 ToolProgress(ToolProgressMessage),
53
54 CommandLifecycle(CommandLifecycleMessage),
56
57 AuthStatus(AuthStatusMessage),
59
60 ToolUseSummary(ToolUseSummaryMessage),
62
63 PromptSuggestion(PromptSuggestionMessage),
65
66 ConversationReset(ConversationResetMessage),
68
69 Progress(TranscriptMessage),
71
72 #[serde(rename = "queue-operation")]
74 QueueOperation(TranscriptMessage),
75
76 #[serde(rename = "pr-link")]
78 PrLink(TranscriptMessage),
79
80 #[serde(rename = "file-history-snapshot")]
82 FileHistorySnapshot(TranscriptMessage),
83
84 Summary(TranscriptMessage),
86
87 Mode(TranscriptMessage),
89
90 #[serde(rename = "permission-mode")]
92 PermissionMode(TranscriptMessage),
93
94 Attachment(TranscriptMessage),
96
97 #[serde(rename = "ai-title")]
99 AiTitle(TranscriptMessage),
100
101 #[serde(rename = "last-prompt")]
103 LastPrompt(TranscriptMessage),
104
105 Started(TranscriptMessage),
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct StreamEventMessage {
111 pub event: Value,
112 pub parent_tool_use_id: Option<String>,
113 pub uuid: String,
114 pub session_id: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub ttft_ms: Option<u64>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub user_message_uuid: Option<String>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub user_message_uuids: Vec<String>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub resume_reason: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub thinking_display: Option<String>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub abandoned_blocks: Option<AbandonedBlocks>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub api_message_id: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub text_runs_joined: Option<bool>,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct AbandonedBlocks {
184 pub api_message_id: String,
188 pub from_block_index: u64,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ToolProgressMessage {
196 pub tool_use_id: String,
197 pub tool_name: String,
198 pub parent_tool_use_id: Option<String>,
199 pub elapsed_time_seconds: f64,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub task_id: Option<String>,
202 pub uuid: String,
203 pub session_id: String,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub heartbeat: Option<bool>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub subagent_type: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub subagent_retry: Option<SubagentRetry>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SubagentRetry {
220 pub agent_id: String,
221 pub attempt: u64,
222 pub max_retries: u64,
223 pub retry_delay_ms: u64,
224 pub error_status: Option<u16>,
225 pub error_category: String,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct CommandLifecycleMessage {
237 pub command_uuid: String,
240 pub state: CommandLifecycleState,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub ran_ahead: Option<bool>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub wakeup_source: Option<CommandWakeupSource>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub scheduled_for: Option<f64>,
264 pub uuid: String,
265 pub session_id: String,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Hash)]
271pub enum CommandWakeupSource {
272 LoopWakeup,
274 ScheduleWakeup,
277 Unknown(String),
279}
280
281impl CommandWakeupSource {
282 pub fn as_str(&self) -> &str {
283 match self {
284 Self::LoopWakeup => "loop_wakeup",
285 Self::ScheduleWakeup => "schedule_wakeup",
286 Self::Unknown(s) => s.as_str(),
287 }
288 }
289}
290
291impl std::fmt::Display for CommandWakeupSource {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 f.write_str(self.as_str())
294 }
295}
296
297impl From<&str> for CommandWakeupSource {
298 fn from(s: &str) -> Self {
299 match s {
300 "loop_wakeup" => Self::LoopWakeup,
301 "schedule_wakeup" => Self::ScheduleWakeup,
302 other => Self::Unknown(other.to_string()),
303 }
304 }
305}
306
307impl Serialize for CommandWakeupSource {
308 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
309 serializer.serialize_str(self.as_str())
310 }
311}
312
313impl<'de> Deserialize<'de> for CommandWakeupSource {
314 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
315 let s = String::deserialize(deserializer)?;
316 Ok(Self::from(s.as_str()))
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Hash)]
322pub enum CommandLifecycleState {
323 Queued,
325 Started,
327 Completed,
329 Cancelled,
335 Discarded,
337 Unknown(String),
339}
340
341impl CommandLifecycleState {
342 pub fn as_str(&self) -> &str {
343 match self {
344 Self::Queued => "queued",
345 Self::Started => "started",
346 Self::Completed => "completed",
347 Self::Cancelled => "cancelled",
348 Self::Discarded => "discarded",
349 Self::Unknown(s) => s.as_str(),
350 }
351 }
352}
353
354impl std::fmt::Display for CommandLifecycleState {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 f.write_str(self.as_str())
357 }
358}
359
360impl From<&str> for CommandLifecycleState {
361 fn from(s: &str) -> Self {
362 match s {
363 "queued" => Self::Queued,
364 "started" => Self::Started,
365 "completed" => Self::Completed,
366 "cancelled" => Self::Cancelled,
367 "discarded" => Self::Discarded,
368 other => Self::Unknown(other.to_string()),
369 }
370 }
371}
372
373impl Serialize for CommandLifecycleState {
374 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
375 serializer.serialize_str(self.as_str())
376 }
377}
378
379impl<'de> Deserialize<'de> for CommandLifecycleState {
380 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
381 let s = String::deserialize(deserializer)?;
382 Ok(Self::from(s.as_str()))
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct AuthStatusMessage {
388 #[serde(rename = "isAuthenticating")]
389 pub is_authenticating: bool,
390 pub output: Vec<String>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub error: Option<String>,
393 pub uuid: String,
394 pub session_id: String,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct ToolUseSummaryMessage {
399 pub summary: String,
400 pub preceding_tool_use_ids: Vec<String>,
401 pub uuid: String,
402 pub session_id: String,
403 #[serde(skip_serializing_if = "Option::is_none")]
404 pub timestamp: Option<String>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct PromptSuggestionMessage {
409 pub suggestion: String,
410 pub uuid: String,
411 pub session_id: String,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct ConversationResetMessage {
433 pub new_conversation_id: String,
437 pub uuid: String,
438 pub session_id: String,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub trigger: Option<ConversationResetTrigger>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub user_message_uuid: Option<String>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub timestamp: Option<String>,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Hash)]
465pub enum ConversationResetTrigger {
466 Clear,
468 PlanModeExit,
470 FreshSession,
472 Onboarding,
474 Unknown(String),
476}
477
478impl ConversationResetTrigger {
479 pub fn as_str(&self) -> &str {
480 match self {
481 Self::Clear => "clear",
482 Self::PlanModeExit => "plan_mode_exit",
483 Self::FreshSession => "fresh_session",
484 Self::Onboarding => "onboarding",
485 Self::Unknown(s) => s.as_str(),
486 }
487 }
488}
489
490impl std::fmt::Display for ConversationResetTrigger {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 f.write_str(self.as_str())
493 }
494}
495
496impl From<&str> for ConversationResetTrigger {
497 fn from(s: &str) -> Self {
498 match s {
499 "clear" => Self::Clear,
500 "plan_mode_exit" => Self::PlanModeExit,
501 "fresh_session" => Self::FreshSession,
502 "onboarding" => Self::Onboarding,
503 other => Self::Unknown(other.to_string()),
504 }
505 }
506}
507
508impl Serialize for ConversationResetTrigger {
509 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
510 serializer.serialize_str(self.as_str())
511 }
512}
513
514impl<'de> Deserialize<'de> for ConversationResetTrigger {
515 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
516 let s = String::deserialize(deserializer)?;
517 Ok(Self::from(s.as_str()))
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct TranscriptMessage {
529 #[serde(flatten)]
530 pub data: Map<String, Value>,
531}
532
533impl ClaudeOutput {
534 pub fn message_type(&self) -> String {
536 match self {
537 ClaudeOutput::System(_) => "system".to_string(),
538 ClaudeOutput::User(_) => "user".to_string(),
539 ClaudeOutput::Assistant(_) => "assistant".to_string(),
540 ClaudeOutput::Result(_) => "result".to_string(),
541 ClaudeOutput::TranscriptResult(_) => "result".to_string(),
542 ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
543 ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
544 ClaudeOutput::ControlCancelRequest(_) => "control_cancel_request".to_string(),
545 ClaudeOutput::Error(_) => "error".to_string(),
546 ClaudeOutput::RateLimitEvent(_) => "rate_limit_event".to_string(),
547 ClaudeOutput::StreamEvent(_) => "stream_event".to_string(),
548 ClaudeOutput::ToolProgress(_) => "tool_progress".to_string(),
549 ClaudeOutput::CommandLifecycle(_) => "command_lifecycle".to_string(),
550 ClaudeOutput::AuthStatus(_) => "auth_status".to_string(),
551 ClaudeOutput::ToolUseSummary(_) => "tool_use_summary".to_string(),
552 ClaudeOutput::PromptSuggestion(_) => "prompt_suggestion".to_string(),
553 ClaudeOutput::ConversationReset(_) => "conversation_reset".to_string(),
554 ClaudeOutput::Progress(_) => "progress".to_string(),
555 ClaudeOutput::QueueOperation(_) => "queue-operation".to_string(),
556 ClaudeOutput::PrLink(_) => "pr-link".to_string(),
557 ClaudeOutput::FileHistorySnapshot(_) => "file-history-snapshot".to_string(),
558 ClaudeOutput::Summary(_) => "summary".to_string(),
559 ClaudeOutput::Mode(_) => "mode".to_string(),
560 ClaudeOutput::PermissionMode(_) => "permission-mode".to_string(),
561 ClaudeOutput::Attachment(_) => "attachment".to_string(),
562 ClaudeOutput::AiTitle(_) => "ai-title".to_string(),
563 ClaudeOutput::LastPrompt(_) => "last-prompt".to_string(),
564 ClaudeOutput::Started(_) => "started".to_string(),
565 }
566 }
567
568 pub fn is_control_request(&self) -> bool {
570 matches!(self, ClaudeOutput::ControlRequest(_))
571 }
572
573 pub fn is_control_response(&self) -> bool {
575 matches!(self, ClaudeOutput::ControlResponse(_))
576 }
577
578 pub fn is_api_error(&self) -> bool {
580 matches!(self, ClaudeOutput::Error(_))
581 }
582
583 pub fn as_control_request(&self) -> Option<&ControlRequest> {
585 match self {
586 ClaudeOutput::ControlRequest(req) => Some(req),
587 _ => None,
588 }
589 }
590
591 pub fn is_control_cancel_request(&self) -> bool {
593 matches!(self, ClaudeOutput::ControlCancelRequest(_))
594 }
595
596 pub fn as_control_cancel_request(&self) -> Option<&ControlCancelRequest> {
598 match self {
599 ClaudeOutput::ControlCancelRequest(req) => Some(req),
600 _ => None,
601 }
602 }
603
604 pub fn as_anthropic_error(&self) -> Option<&AnthropicError> {
620 match self {
621 ClaudeOutput::Error(err) => Some(err),
622 _ => None,
623 }
624 }
625
626 pub fn is_rate_limit_event(&self) -> bool {
628 matches!(self, ClaudeOutput::RateLimitEvent(_))
629 }
630
631 pub fn as_rate_limit_event(&self) -> Option<&RateLimitEvent> {
633 match self {
634 ClaudeOutput::RateLimitEvent(evt) => Some(evt),
635 _ => None,
636 }
637 }
638
639 pub fn is_error(&self) -> bool {
641 matches!(self, ClaudeOutput::Result(r) if r.is_error)
642 }
643
644 pub fn is_assistant_message(&self) -> bool {
646 matches!(self, ClaudeOutput::Assistant(_))
647 }
648
649 pub fn is_system_message(&self) -> bool {
651 matches!(self, ClaudeOutput::System(_))
652 }
653
654 pub fn is_system_init(&self) -> bool {
665 matches!(self, ClaudeOutput::System(sys) if sys.is_init())
666 }
667
668 pub fn session_id(&self) -> Option<&str> {
684 match self {
685 ClaudeOutput::System(sys) => sys
686 .data
687 .get("session_id")
688 .or_else(|| sys.data.get("sessionId"))
689 .and_then(|v| v.as_str()),
690 ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
691 ClaudeOutput::Result(res) => Some(&res.session_id),
692 ClaudeOutput::TranscriptResult(msg) => msg
693 .data
694 .get("session_id")
695 .or_else(|| msg.data.get("sessionId"))
696 .and_then(|v| v.as_str()),
697 ClaudeOutput::User(_) => None,
698 ClaudeOutput::ControlRequest(_) => None,
699 ClaudeOutput::ControlResponse(_) => None,
700 ClaudeOutput::ControlCancelRequest(msg) => msg.session_id.as_deref(),
701 ClaudeOutput::Error(_) => None,
702 ClaudeOutput::RateLimitEvent(evt) => Some(&evt.session_id),
703 ClaudeOutput::StreamEvent(msg) => Some(&msg.session_id),
704 ClaudeOutput::ToolProgress(msg) => Some(&msg.session_id),
705 ClaudeOutput::CommandLifecycle(msg) => Some(&msg.session_id),
706 ClaudeOutput::AuthStatus(msg) => Some(&msg.session_id),
707 ClaudeOutput::ToolUseSummary(msg) => Some(&msg.session_id),
708 ClaudeOutput::PromptSuggestion(msg) => Some(&msg.session_id),
709 ClaudeOutput::ConversationReset(msg) => Some(&msg.session_id),
710 ClaudeOutput::Progress(msg)
711 | ClaudeOutput::QueueOperation(msg)
712 | ClaudeOutput::PrLink(msg)
713 | ClaudeOutput::FileHistorySnapshot(msg)
714 | ClaudeOutput::Summary(msg)
715 | ClaudeOutput::Mode(msg)
716 | ClaudeOutput::PermissionMode(msg)
717 | ClaudeOutput::Attachment(msg)
718 | ClaudeOutput::AiTitle(msg)
719 | ClaudeOutput::LastPrompt(msg)
720 | ClaudeOutput::Started(msg) => msg
721 .data
722 .get("session_id")
723 .or_else(|| msg.data.get("sessionId"))
724 .and_then(|v| v.as_str()),
725 }
726 }
727
728 pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
747 match self {
748 ClaudeOutput::Assistant(ass) => {
749 ass.message.content.iter().find_map(|block| match block {
750 ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
751 _ => None,
752 })
753 }
754 _ => None,
755 }
756 }
757
758 pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
778 let content = match self {
779 ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
780 _ => None,
781 };
782
783 content
784 .into_iter()
785 .flat_map(|c| c.iter())
786 .filter_map(|block| match block {
787 ContentBlock::ToolUse(tu) => Some(tu),
788 _ => None,
789 })
790 }
791
792 pub fn text_content(&self) -> Option<String> {
808 match self {
809 ClaudeOutput::Assistant(ass) => {
810 let texts: Vec<&str> = ass
811 .message
812 .content
813 .iter()
814 .filter_map(|block| match block {
815 ContentBlock::Text(t) => Some(t.text.as_str()),
816 _ => None,
817 })
818 .collect();
819
820 if texts.is_empty() {
821 None
822 } else {
823 Some(texts.join(""))
824 }
825 }
826 _ => None,
827 }
828 }
829
830 pub fn as_assistant(&self) -> Option<&AssistantMessage> {
845 match self {
846 ClaudeOutput::Assistant(ass) => Some(ass),
847 _ => None,
848 }
849 }
850
851 pub fn as_result(&self) -> Option<&ResultMessage> {
867 match self {
868 ClaudeOutput::Result(res) => Some(res),
869 _ => None,
870 }
871 }
872
873 pub fn as_system(&self) -> Option<&SystemMessage> {
875 match self {
876 ClaudeOutput::System(sys) => Some(sys),
877 _ => None,
878 }
879 }
880
881 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
886 match Self::parse_json(s) {
888 Ok(output) => Ok(output),
889 Err(first_error) => {
890 if let Some(json_start) = s.find('{') {
892 let trimmed = &s[json_start..];
893 match Self::parse_json(trimmed) {
894 Ok(output) => Ok(output),
895 Err(_) => {
896 Err(first_error)
898 }
899 }
900 } else {
901 Err(first_error)
902 }
903 }
904 }
905 }
906
907 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
909 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
911 raw_line: s.to_string(),
912 raw_json: None,
913 error_message: format!("Invalid JSON: {}", e),
914 })?;
915
916 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
918 raw_line: s.to_string(),
919 raw_json: Some(value),
920 error_message: e.to_string(),
921 })
922 }
923}
924
925impl<'de> Deserialize<'de> for ClaudeOutput {
926 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
927 let value = Value::deserialize(deserializer)?;
928 let message_type = value
929 .get("type")
930 .and_then(|v| v.as_str())
931 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
932 let mut payload = value.clone();
933 if let Some(obj) = payload.as_object_mut() {
934 obj.remove("type");
935 }
936
937 fn parse<T, E>(value: Value) -> Result<T, E>
938 where
939 T: serde::de::DeserializeOwned,
940 E: serde::de::Error,
941 {
942 serde_json::from_value(value).map_err(E::custom)
943 }
944
945 match message_type {
946 "system" => parse(payload).map(Self::System),
947 "user" => parse(payload).map(Self::User),
948 "assistant" => parse(payload).map(Self::Assistant),
949 "result" if value.get("subtype").is_some() => parse(payload).map(Self::Result),
950 "result" => parse(payload).map(Self::TranscriptResult),
951 "control_request" => parse(payload).map(Self::ControlRequest),
952 "control_response" => parse(payload).map(Self::ControlResponse),
953 "control_cancel_request" => parse(payload).map(Self::ControlCancelRequest),
954 "error" => parse(payload).map(Self::Error),
955 "rate_limit_event" => parse(payload).map(Self::RateLimitEvent),
956 "stream_event" => parse(payload).map(Self::StreamEvent),
957 "tool_progress" => parse(payload).map(Self::ToolProgress),
958 "command_lifecycle" => parse(payload).map(Self::CommandLifecycle),
959 "auth_status" => parse(payload).map(Self::AuthStatus),
960 "tool_use_summary" => parse(payload).map(Self::ToolUseSummary),
961 "prompt_suggestion" => parse(payload).map(Self::PromptSuggestion),
962 "conversation_reset" => parse(payload).map(Self::ConversationReset),
963 "progress" => parse(payload).map(Self::Progress),
964 "queue-operation" => parse(payload).map(Self::QueueOperation),
965 "pr-link" => parse(payload).map(Self::PrLink),
966 "file-history-snapshot" => parse(payload).map(Self::FileHistorySnapshot),
967 "summary" => parse(payload).map(Self::Summary),
968 "mode" => parse(payload).map(Self::Mode),
969 "permission-mode" => parse(payload).map(Self::PermissionMode),
970 "attachment" => parse(payload).map(Self::Attachment),
971 "ai-title" => parse(payload).map(Self::AiTitle),
972 "last-prompt" => parse(payload).map(Self::LastPrompt),
973 "started" => parse(payload).map(Self::Started),
974 other => Err(serde::de::Error::unknown_variant(
975 other,
976 &[
977 "system",
978 "user",
979 "assistant",
980 "result",
981 "control_request",
982 "control_response",
983 "control_cancel_request",
984 "error",
985 "rate_limit_event",
986 "stream_event",
987 "tool_progress",
988 "command_lifecycle",
989 "auth_status",
990 "tool_use_summary",
991 "prompt_suggestion",
992 "conversation_reset",
993 "progress",
994 "queue-operation",
995 "pr-link",
996 "file-history-snapshot",
997 "summary",
998 "mode",
999 "permission-mode",
1000 "attachment",
1001 "ai-title",
1002 "last-prompt",
1003 "started",
1004 ],
1005 )),
1006 }
1007 }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::*;
1013
1014 #[test]
1015 fn test_deserialize_control_cancel_request() {
1016 let json = r#"{"request_id":"00aea85d-70f9-4cc4-becc-b0f05f5037f3","type":"control_cancel_request"}"#;
1017 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1018 assert!(output.is_control_cancel_request());
1019 assert_eq!(output.message_type(), "control_cancel_request");
1020 assert_eq!(output.session_id(), None);
1021 let cancel = output.as_control_cancel_request().unwrap();
1022 assert_eq!(cancel.request_id, "00aea85d-70f9-4cc4-becc-b0f05f5037f3");
1023
1024 let round_trip: Value = serde_json::to_value(&output).unwrap();
1025 assert_eq!(round_trip, serde_json::from_str::<Value>(json).unwrap());
1026
1027 let bridged = r#"{"type":"control_cancel_request","request_id":"r1","session_id":"s1"}"#;
1028 let output: ClaudeOutput = serde_json::from_str(bridged).unwrap();
1029 assert_eq!(output.session_id(), Some("s1"));
1030 }
1031
1032 #[test]
1033 fn test_deserialize_assistant_message() {
1034 let json = r#"{
1035 "type": "assistant",
1036 "message": {
1037 "id": "msg_123",
1038 "role": "assistant",
1039 "model": "claude-3-sonnet",
1040 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1041 },
1042 "session_id": "123"
1043 }"#;
1044
1045 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1046 assert!(output.is_assistant_message());
1047 }
1048
1049 #[test]
1050 fn test_deserialize_new_top_level_message_types() {
1051 let cases = [
1052 (
1053 r#"{"type":"stream_event","event":{"type":"content_block_delta"},"parent_tool_use_id":null,"uuid":"u1","session_id":"s1","ttft_ms":12}"#,
1054 "stream_event",
1055 ),
1056 (
1057 r#"{"type":"tool_progress","tool_use_id":"toolu_1","tool_name":"Bash","parent_tool_use_id":null,"elapsed_time_seconds":1.25,"task_id":"task-1","uuid":"u2","session_id":"s2"}"#,
1058 "tool_progress",
1059 ),
1060 (
1061 r#"{"type":"auth_status","isAuthenticating":true,"output":["login"],"uuid":"u3","session_id":"s3"}"#,
1062 "auth_status",
1063 ),
1064 (
1065 r#"{"type":"tool_use_summary","summary":"read files","preceding_tool_use_ids":["toolu_1"],"uuid":"u4","session_id":"s4","timestamp":"2026-07-09T17:46:33Z"}"#,
1066 "tool_use_summary",
1067 ),
1068 (
1069 r#"{"type":"prompt_suggestion","suggestion":"Run tests","uuid":"u5","session_id":"s5"}"#,
1070 "prompt_suggestion",
1071 ),
1072 (
1073 r#"{"type":"conversation_reset","new_conversation_id":"new-session","uuid":"u6","session_id":"s6"}"#,
1074 "conversation_reset",
1075 ),
1076 (
1077 r#"{"type":"command_lifecycle","command_uuid":"cmd-1","state":"queued","uuid":"u7","session_id":"s7"}"#,
1078 "command_lifecycle",
1079 ),
1080 ];
1081
1082 for (json, message_type) in cases {
1083 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1084 assert_eq!(output.message_type(), message_type);
1085 assert!(output.session_id().is_some());
1086 }
1087 }
1088
1089 #[test]
1090 fn test_command_lifecycle_states_roundtrip() {
1091 for state in ["queued", "started", "completed", "cancelled", "discarded"] {
1092 let json = format!(
1093 r#"{{"type":"command_lifecycle","command_uuid":"cmd-1","state":"{}","uuid":"u1","session_id":"s1"}}"#,
1094 state
1095 );
1096 let output: ClaudeOutput = serde_json::from_str(&json).unwrap();
1097 let ClaudeOutput::CommandLifecycle(msg) = &output else {
1098 panic!("expected CommandLifecycle");
1099 };
1100 assert_eq!(msg.state.as_str(), state);
1101 assert!(!matches!(msg.state, CommandLifecycleState::Unknown(_)));
1102 assert_eq!(serde_json::to_string(&output).unwrap(), json);
1103 }
1104
1105 let json = r#"{"type":"command_lifecycle","command_uuid":"cmd-2","state":"parked","uuid":"u2","session_id":"s2"}"#;
1107 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1108 let ClaudeOutput::CommandLifecycle(msg) = &output else {
1109 panic!("expected CommandLifecycle");
1110 };
1111 assert_eq!(
1112 msg.state,
1113 CommandLifecycleState::Unknown("parked".to_string())
1114 );
1115 assert_eq!(serde_json::to_string(&output).unwrap(), json);
1116 }
1117
1118 #[test]
1119 fn test_tool_progress_heartbeat_and_subagent_retry() {
1120 let json = r#"{"type":"tool_progress","tool_use_id":"toolu_1","tool_name":"Task","parent_tool_use_id":null,"elapsed_time_seconds":30,"uuid":"u1","session_id":"s1","heartbeat":true,"subagent_type":"Explore","subagent_retry":{"agent_id":"agent-1","attempt":2,"max_retries":5,"retry_delay_ms":4000,"error_status":529,"error_category":"overloaded"}}"#;
1121 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1122 let ClaudeOutput::ToolProgress(msg) = &output else {
1123 panic!("expected ToolProgress");
1124 };
1125 assert_eq!(msg.heartbeat, Some(true));
1126 assert_eq!(msg.subagent_type.as_deref(), Some("Explore"));
1127 let retry = msg.subagent_retry.as_ref().unwrap();
1128 assert_eq!(retry.attempt, 2);
1129 assert_eq!(retry.error_status, Some(529));
1130 assert_eq!(retry.error_category, "overloaded");
1131
1132 let json = r#"{"type":"tool_progress","tool_use_id":"toolu_2","tool_name":"Task","parent_tool_use_id":null,"elapsed_time_seconds":1,"uuid":"u2","session_id":"s2","subagent_retry":{"agent_id":"agent-2","attempt":1,"max_retries":3,"retry_delay_ms":500,"error_status":null,"error_category":"network"}}"#;
1134 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1135 let ClaudeOutput::ToolProgress(msg) = &output else {
1136 panic!("expected ToolProgress");
1137 };
1138 assert_eq!(msg.subagent_retry.as_ref().unwrap().error_status, None);
1139 }
1140
1141 #[test]
1142 fn test_deserialize_transcript_only_message_types() {
1143 let cases = [
1144 (
1145 r#"{"type":"progress","sessionId":"s1","content":"delta"}"#,
1146 "progress",
1147 Some("s1"),
1148 ),
1149 (
1150 r#"{"type":"queue-operation","sessionId":"s2","operation":"push"}"#,
1151 "queue-operation",
1152 Some("s2"),
1153 ),
1154 (
1155 r#"{"type":"pr-link","sessionId":"s3","url":"https://example.invalid/pr"}"#,
1156 "pr-link",
1157 Some("s3"),
1158 ),
1159 (
1160 r#"{"type":"file-history-snapshot","sessionId":"s4","files":[]}"#,
1161 "file-history-snapshot",
1162 Some("s4"),
1163 ),
1164 (r#"{"type":"summary","summary":"Done"}"#, "summary", None),
1165 (
1166 r#"{"type":"mode","mode":"normal","sessionId":"s5"}"#,
1167 "mode",
1168 Some("s5"),
1169 ),
1170 (
1171 r#"{"type":"permission-mode","permissionMode":"default","sessionId":"s6"}"#,
1172 "permission-mode",
1173 Some("s6"),
1174 ),
1175 (
1176 r#"{"type":"attachment","attachment":{"type":"task_reminder"},"sessionId":"s7"}"#,
1177 "attachment",
1178 Some("s7"),
1179 ),
1180 (
1181 r#"{"type":"ai-title","aiTitle":"Title","sessionId":"s8"}"#,
1182 "ai-title",
1183 Some("s8"),
1184 ),
1185 (
1186 r#"{"type":"last-prompt","lastPrompt":"prompt","sessionId":"s9"}"#,
1187 "last-prompt",
1188 Some("s9"),
1189 ),
1190 (
1191 r#"{"type":"started","sessionId":"s10"}"#,
1192 "started",
1193 Some("s10"),
1194 ),
1195 (
1196 r#"{"type":"result","key":"k","agentId":"a","result":{"ok":true}}"#,
1197 "result",
1198 None,
1199 ),
1200 ];
1201
1202 for (json, message_type, session_id) in cases {
1203 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1204 assert_eq!(output.message_type(), message_type);
1205 assert_eq!(output.session_id(), session_id);
1206 }
1207 }
1208
1209 #[test]
1210 fn test_transcript_assistant_accepts_camel_case_session_id() {
1211 let json = r#"{
1212 "type": "assistant",
1213 "sessionId": "camel-session",
1214 "message": {
1215 "id": "msg_123",
1216 "role": "assistant",
1217 "model": "claude-3-sonnet",
1218 "content": [{"type": "text", "text": "Hello"}]
1219 }
1220 }"#;
1221
1222 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1223 assert_eq!(output.session_id(), Some("camel-session"));
1224 }
1225
1226 #[test]
1227 fn test_is_system_init() {
1228 let init_json = r#"{
1229 "type": "system",
1230 "subtype": "init",
1231 "session_id": "test-session"
1232 }"#;
1233 let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
1234 assert!(output.is_system_init());
1235
1236 let status_json = r#"{
1237 "type": "system",
1238 "subtype": "status",
1239 "session_id": "test-session"
1240 }"#;
1241 let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
1242 assert!(!output.is_system_init());
1243 }
1244
1245 #[test]
1246 fn test_session_id() {
1247 let result_json = r#"{
1249 "type": "result",
1250 "subtype": "success",
1251 "is_error": false,
1252 "duration_ms": 100,
1253 "duration_api_ms": 200,
1254 "num_turns": 1,
1255 "session_id": "result-session",
1256 "total_cost_usd": 0.01
1257 }"#;
1258 let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1259 assert_eq!(output.session_id(), Some("result-session"));
1260
1261 let assistant_json = r#"{
1263 "type": "assistant",
1264 "message": {
1265 "id": "msg_1",
1266 "role": "assistant",
1267 "model": "claude-3",
1268 "content": []
1269 },
1270 "session_id": "assistant-session"
1271 }"#;
1272 let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1273 assert_eq!(output.session_id(), Some("assistant-session"));
1274
1275 let system_json = r#"{
1277 "type": "system",
1278 "subtype": "init",
1279 "session_id": "system-session"
1280 }"#;
1281 let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
1282 assert_eq!(output.session_id(), Some("system-session"));
1283 }
1284
1285 #[test]
1286 fn test_as_tool_use() {
1287 let json = r#"{
1288 "type": "assistant",
1289 "message": {
1290 "id": "msg_1",
1291 "role": "assistant",
1292 "model": "claude-3",
1293 "content": [
1294 {"type": "text", "text": "Let me run that command."},
1295 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
1296 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
1297 ]
1298 },
1299 "session_id": "abc"
1300 }"#;
1301 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1302
1303 let bash = output.as_tool_use("Bash");
1305 assert!(bash.is_some());
1306 assert_eq!(bash.unwrap().id, "tu_1");
1307
1308 let read = output.as_tool_use("Read");
1310 assert!(read.is_some());
1311 assert_eq!(read.unwrap().id, "tu_2");
1312
1313 assert!(output.as_tool_use("Write").is_none());
1315
1316 let result_json = r#"{
1318 "type": "result",
1319 "subtype": "success",
1320 "is_error": false,
1321 "duration_ms": 100,
1322 "duration_api_ms": 200,
1323 "num_turns": 1,
1324 "session_id": "abc",
1325 "total_cost_usd": 0.01
1326 }"#;
1327 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1328 assert!(result.as_tool_use("Bash").is_none());
1329 }
1330
1331 #[test]
1332 fn test_tool_uses() {
1333 let json = r#"{
1334 "type": "assistant",
1335 "message": {
1336 "id": "msg_1",
1337 "role": "assistant",
1338 "model": "claude-3",
1339 "content": [
1340 {"type": "text", "text": "Running commands..."},
1341 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
1342 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
1343 {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
1344 ]
1345 },
1346 "session_id": "abc"
1347 }"#;
1348 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1349
1350 let tools: Vec<_> = output.tool_uses().collect();
1351 assert_eq!(tools.len(), 3);
1352 assert_eq!(tools[0].name, "Bash");
1353 assert_eq!(tools[1].name, "Read");
1354 assert_eq!(tools[2].name, "Write");
1355 }
1356
1357 #[test]
1358 fn test_text_content() {
1359 let json = r#"{
1361 "type": "assistant",
1362 "message": {
1363 "id": "msg_1",
1364 "role": "assistant",
1365 "model": "claude-3",
1366 "content": [{"type": "text", "text": "Hello, world!"}]
1367 },
1368 "session_id": "abc"
1369 }"#;
1370 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1371 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1372
1373 let json = r#"{
1375 "type": "assistant",
1376 "message": {
1377 "id": "msg_1",
1378 "role": "assistant",
1379 "model": "claude-3",
1380 "content": [
1381 {"type": "text", "text": "Hello, "},
1382 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
1383 {"type": "text", "text": "world!"}
1384 ]
1385 },
1386 "session_id": "abc"
1387 }"#;
1388 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1389 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1390
1391 let json = r#"{
1393 "type": "assistant",
1394 "message": {
1395 "id": "msg_1",
1396 "role": "assistant",
1397 "model": "claude-3",
1398 "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
1399 },
1400 "session_id": "abc"
1401 }"#;
1402 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1403 assert_eq!(output.text_content(), None);
1404
1405 let json = r#"{
1407 "type": "result",
1408 "subtype": "success",
1409 "is_error": false,
1410 "duration_ms": 100,
1411 "duration_api_ms": 200,
1412 "num_turns": 1,
1413 "session_id": "abc",
1414 "total_cost_usd": 0.01
1415 }"#;
1416 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1417 assert_eq!(output.text_content(), None);
1418 }
1419
1420 #[test]
1421 fn test_as_assistant() {
1422 let json = r#"{
1423 "type": "assistant",
1424 "message": {
1425 "id": "msg_1",
1426 "role": "assistant",
1427 "model": "claude-sonnet-4",
1428 "content": []
1429 },
1430 "session_id": "abc"
1431 }"#;
1432 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1433
1434 let assistant = output.as_assistant();
1435 assert!(assistant.is_some());
1436 assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
1437
1438 let result_json = r#"{
1440 "type": "result",
1441 "subtype": "success",
1442 "is_error": false,
1443 "duration_ms": 100,
1444 "duration_api_ms": 200,
1445 "num_turns": 1,
1446 "session_id": "abc",
1447 "total_cost_usd": 0.01
1448 }"#;
1449 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1450 assert!(result.as_assistant().is_none());
1451 }
1452
1453 #[test]
1454 fn test_as_result() {
1455 let json = r#"{
1456 "type": "result",
1457 "subtype": "success",
1458 "is_error": false,
1459 "duration_ms": 100,
1460 "duration_api_ms": 200,
1461 "num_turns": 5,
1462 "session_id": "abc",
1463 "total_cost_usd": 0.05
1464 }"#;
1465 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1466
1467 let result = output.as_result();
1468 assert!(result.is_some());
1469 assert_eq!(result.unwrap().num_turns, 5);
1470 assert_eq!(result.unwrap().total_cost_usd, 0.05);
1471
1472 let assistant_json = r#"{
1474 "type": "assistant",
1475 "message": {
1476 "id": "msg_1",
1477 "role": "assistant",
1478 "model": "claude-3",
1479 "content": []
1480 },
1481 "session_id": "abc"
1482 }"#;
1483 let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1484 assert!(assistant.as_result().is_none());
1485 }
1486
1487 #[test]
1488 fn test_as_system() {
1489 let json = r#"{
1490 "type": "system",
1491 "subtype": "init",
1492 "session_id": "abc",
1493 "model": "claude-3"
1494 }"#;
1495 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1496
1497 let system = output.as_system();
1498 assert!(system.is_some());
1499 assert!(system.unwrap().is_init());
1500
1501 let result_json = r#"{
1503 "type": "result",
1504 "subtype": "success",
1505 "is_error": false,
1506 "duration_ms": 100,
1507 "duration_api_ms": 200,
1508 "num_turns": 1,
1509 "session_id": "abc",
1510 "total_cost_usd": 0.01
1511 }"#;
1512 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1513 assert!(result.as_system().is_none());
1514 }
1515}