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,
332 Discarded,
334 Unknown(String),
336}
337
338impl CommandLifecycleState {
339 pub fn as_str(&self) -> &str {
340 match self {
341 Self::Queued => "queued",
342 Self::Started => "started",
343 Self::Completed => "completed",
344 Self::Cancelled => "cancelled",
345 Self::Discarded => "discarded",
346 Self::Unknown(s) => s.as_str(),
347 }
348 }
349}
350
351impl std::fmt::Display for CommandLifecycleState {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 f.write_str(self.as_str())
354 }
355}
356
357impl From<&str> for CommandLifecycleState {
358 fn from(s: &str) -> Self {
359 match s {
360 "queued" => Self::Queued,
361 "started" => Self::Started,
362 "completed" => Self::Completed,
363 "cancelled" => Self::Cancelled,
364 "discarded" => Self::Discarded,
365 other => Self::Unknown(other.to_string()),
366 }
367 }
368}
369
370impl Serialize for CommandLifecycleState {
371 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
372 serializer.serialize_str(self.as_str())
373 }
374}
375
376impl<'de> Deserialize<'de> for CommandLifecycleState {
377 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
378 let s = String::deserialize(deserializer)?;
379 Ok(Self::from(s.as_str()))
380 }
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct AuthStatusMessage {
385 #[serde(rename = "isAuthenticating")]
386 pub is_authenticating: bool,
387 pub output: Vec<String>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub error: Option<String>,
390 pub uuid: String,
391 pub session_id: String,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct ToolUseSummaryMessage {
396 pub summary: String,
397 pub preceding_tool_use_ids: Vec<String>,
398 pub uuid: String,
399 pub session_id: String,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub timestamp: Option<String>,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct PromptSuggestionMessage {
406 pub suggestion: String,
407 pub uuid: String,
408 pub session_id: String,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct ConversationResetMessage {
430 pub new_conversation_id: String,
434 pub uuid: String,
435 pub session_id: String,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub trigger: Option<ConversationResetTrigger>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub user_message_uuid: Option<String>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub timestamp: Option<String>,
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Hash)]
462pub enum ConversationResetTrigger {
463 Clear,
465 PlanModeExit,
467 FreshSession,
469 Onboarding,
471 Unknown(String),
473}
474
475impl ConversationResetTrigger {
476 pub fn as_str(&self) -> &str {
477 match self {
478 Self::Clear => "clear",
479 Self::PlanModeExit => "plan_mode_exit",
480 Self::FreshSession => "fresh_session",
481 Self::Onboarding => "onboarding",
482 Self::Unknown(s) => s.as_str(),
483 }
484 }
485}
486
487impl std::fmt::Display for ConversationResetTrigger {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.write_str(self.as_str())
490 }
491}
492
493impl From<&str> for ConversationResetTrigger {
494 fn from(s: &str) -> Self {
495 match s {
496 "clear" => Self::Clear,
497 "plan_mode_exit" => Self::PlanModeExit,
498 "fresh_session" => Self::FreshSession,
499 "onboarding" => Self::Onboarding,
500 other => Self::Unknown(other.to_string()),
501 }
502 }
503}
504
505impl Serialize for ConversationResetTrigger {
506 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
507 serializer.serialize_str(self.as_str())
508 }
509}
510
511impl<'de> Deserialize<'de> for ConversationResetTrigger {
512 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
513 let s = String::deserialize(deserializer)?;
514 Ok(Self::from(s.as_str()))
515 }
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct TranscriptMessage {
526 #[serde(flatten)]
527 pub data: Map<String, Value>,
528}
529
530impl ClaudeOutput {
531 pub fn message_type(&self) -> String {
533 match self {
534 ClaudeOutput::System(_) => "system".to_string(),
535 ClaudeOutput::User(_) => "user".to_string(),
536 ClaudeOutput::Assistant(_) => "assistant".to_string(),
537 ClaudeOutput::Result(_) => "result".to_string(),
538 ClaudeOutput::TranscriptResult(_) => "result".to_string(),
539 ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
540 ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
541 ClaudeOutput::ControlCancelRequest(_) => "control_cancel_request".to_string(),
542 ClaudeOutput::Error(_) => "error".to_string(),
543 ClaudeOutput::RateLimitEvent(_) => "rate_limit_event".to_string(),
544 ClaudeOutput::StreamEvent(_) => "stream_event".to_string(),
545 ClaudeOutput::ToolProgress(_) => "tool_progress".to_string(),
546 ClaudeOutput::CommandLifecycle(_) => "command_lifecycle".to_string(),
547 ClaudeOutput::AuthStatus(_) => "auth_status".to_string(),
548 ClaudeOutput::ToolUseSummary(_) => "tool_use_summary".to_string(),
549 ClaudeOutput::PromptSuggestion(_) => "prompt_suggestion".to_string(),
550 ClaudeOutput::ConversationReset(_) => "conversation_reset".to_string(),
551 ClaudeOutput::Progress(_) => "progress".to_string(),
552 ClaudeOutput::QueueOperation(_) => "queue-operation".to_string(),
553 ClaudeOutput::PrLink(_) => "pr-link".to_string(),
554 ClaudeOutput::FileHistorySnapshot(_) => "file-history-snapshot".to_string(),
555 ClaudeOutput::Summary(_) => "summary".to_string(),
556 ClaudeOutput::Mode(_) => "mode".to_string(),
557 ClaudeOutput::PermissionMode(_) => "permission-mode".to_string(),
558 ClaudeOutput::Attachment(_) => "attachment".to_string(),
559 ClaudeOutput::AiTitle(_) => "ai-title".to_string(),
560 ClaudeOutput::LastPrompt(_) => "last-prompt".to_string(),
561 ClaudeOutput::Started(_) => "started".to_string(),
562 }
563 }
564
565 pub fn is_control_request(&self) -> bool {
567 matches!(self, ClaudeOutput::ControlRequest(_))
568 }
569
570 pub fn is_control_response(&self) -> bool {
572 matches!(self, ClaudeOutput::ControlResponse(_))
573 }
574
575 pub fn is_api_error(&self) -> bool {
577 matches!(self, ClaudeOutput::Error(_))
578 }
579
580 pub fn as_control_request(&self) -> Option<&ControlRequest> {
582 match self {
583 ClaudeOutput::ControlRequest(req) => Some(req),
584 _ => None,
585 }
586 }
587
588 pub fn is_control_cancel_request(&self) -> bool {
590 matches!(self, ClaudeOutput::ControlCancelRequest(_))
591 }
592
593 pub fn as_control_cancel_request(&self) -> Option<&ControlCancelRequest> {
595 match self {
596 ClaudeOutput::ControlCancelRequest(req) => Some(req),
597 _ => None,
598 }
599 }
600
601 pub fn as_anthropic_error(&self) -> Option<&AnthropicError> {
617 match self {
618 ClaudeOutput::Error(err) => Some(err),
619 _ => None,
620 }
621 }
622
623 pub fn is_rate_limit_event(&self) -> bool {
625 matches!(self, ClaudeOutput::RateLimitEvent(_))
626 }
627
628 pub fn as_rate_limit_event(&self) -> Option<&RateLimitEvent> {
630 match self {
631 ClaudeOutput::RateLimitEvent(evt) => Some(evt),
632 _ => None,
633 }
634 }
635
636 pub fn is_error(&self) -> bool {
638 matches!(self, ClaudeOutput::Result(r) if r.is_error)
639 }
640
641 pub fn is_assistant_message(&self) -> bool {
643 matches!(self, ClaudeOutput::Assistant(_))
644 }
645
646 pub fn is_system_message(&self) -> bool {
648 matches!(self, ClaudeOutput::System(_))
649 }
650
651 pub fn is_system_init(&self) -> bool {
662 matches!(self, ClaudeOutput::System(sys) if sys.is_init())
663 }
664
665 pub fn session_id(&self) -> Option<&str> {
681 match self {
682 ClaudeOutput::System(sys) => sys
683 .data
684 .get("session_id")
685 .or_else(|| sys.data.get("sessionId"))
686 .and_then(|v| v.as_str()),
687 ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
688 ClaudeOutput::Result(res) => Some(&res.session_id),
689 ClaudeOutput::TranscriptResult(msg) => msg
690 .data
691 .get("session_id")
692 .or_else(|| msg.data.get("sessionId"))
693 .and_then(|v| v.as_str()),
694 ClaudeOutput::User(_) => None,
695 ClaudeOutput::ControlRequest(_) => None,
696 ClaudeOutput::ControlResponse(_) => None,
697 ClaudeOutput::ControlCancelRequest(msg) => msg.session_id.as_deref(),
698 ClaudeOutput::Error(_) => None,
699 ClaudeOutput::RateLimitEvent(evt) => Some(&evt.session_id),
700 ClaudeOutput::StreamEvent(msg) => Some(&msg.session_id),
701 ClaudeOutput::ToolProgress(msg) => Some(&msg.session_id),
702 ClaudeOutput::CommandLifecycle(msg) => Some(&msg.session_id),
703 ClaudeOutput::AuthStatus(msg) => Some(&msg.session_id),
704 ClaudeOutput::ToolUseSummary(msg) => Some(&msg.session_id),
705 ClaudeOutput::PromptSuggestion(msg) => Some(&msg.session_id),
706 ClaudeOutput::ConversationReset(msg) => Some(&msg.session_id),
707 ClaudeOutput::Progress(msg)
708 | ClaudeOutput::QueueOperation(msg)
709 | ClaudeOutput::PrLink(msg)
710 | ClaudeOutput::FileHistorySnapshot(msg)
711 | ClaudeOutput::Summary(msg)
712 | ClaudeOutput::Mode(msg)
713 | ClaudeOutput::PermissionMode(msg)
714 | ClaudeOutput::Attachment(msg)
715 | ClaudeOutput::AiTitle(msg)
716 | ClaudeOutput::LastPrompt(msg)
717 | ClaudeOutput::Started(msg) => msg
718 .data
719 .get("session_id")
720 .or_else(|| msg.data.get("sessionId"))
721 .and_then(|v| v.as_str()),
722 }
723 }
724
725 pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
744 match self {
745 ClaudeOutput::Assistant(ass) => {
746 ass.message.content.iter().find_map(|block| match block {
747 ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
748 _ => None,
749 })
750 }
751 _ => None,
752 }
753 }
754
755 pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
775 let content = match self {
776 ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
777 _ => None,
778 };
779
780 content
781 .into_iter()
782 .flat_map(|c| c.iter())
783 .filter_map(|block| match block {
784 ContentBlock::ToolUse(tu) => Some(tu),
785 _ => None,
786 })
787 }
788
789 pub fn text_content(&self) -> Option<String> {
805 match self {
806 ClaudeOutput::Assistant(ass) => {
807 let texts: Vec<&str> = ass
808 .message
809 .content
810 .iter()
811 .filter_map(|block| match block {
812 ContentBlock::Text(t) => Some(t.text.as_str()),
813 _ => None,
814 })
815 .collect();
816
817 if texts.is_empty() {
818 None
819 } else {
820 Some(texts.join(""))
821 }
822 }
823 _ => None,
824 }
825 }
826
827 pub fn as_assistant(&self) -> Option<&AssistantMessage> {
842 match self {
843 ClaudeOutput::Assistant(ass) => Some(ass),
844 _ => None,
845 }
846 }
847
848 pub fn as_result(&self) -> Option<&ResultMessage> {
864 match self {
865 ClaudeOutput::Result(res) => Some(res),
866 _ => None,
867 }
868 }
869
870 pub fn as_system(&self) -> Option<&SystemMessage> {
872 match self {
873 ClaudeOutput::System(sys) => Some(sys),
874 _ => None,
875 }
876 }
877
878 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
883 match Self::parse_json(s) {
885 Ok(output) => Ok(output),
886 Err(first_error) => {
887 if let Some(json_start) = s.find('{') {
889 let trimmed = &s[json_start..];
890 match Self::parse_json(trimmed) {
891 Ok(output) => Ok(output),
892 Err(_) => {
893 Err(first_error)
895 }
896 }
897 } else {
898 Err(first_error)
899 }
900 }
901 }
902 }
903
904 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
906 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
908 raw_line: s.to_string(),
909 raw_json: None,
910 error_message: format!("Invalid JSON: {}", e),
911 })?;
912
913 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
915 raw_line: s.to_string(),
916 raw_json: Some(value),
917 error_message: e.to_string(),
918 })
919 }
920}
921
922impl<'de> Deserialize<'de> for ClaudeOutput {
923 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
924 let value = Value::deserialize(deserializer)?;
925 let message_type = value
926 .get("type")
927 .and_then(|v| v.as_str())
928 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
929 let mut payload = value.clone();
930 if let Some(obj) = payload.as_object_mut() {
931 obj.remove("type");
932 }
933
934 fn parse<T, E>(value: Value) -> Result<T, E>
935 where
936 T: serde::de::DeserializeOwned,
937 E: serde::de::Error,
938 {
939 serde_json::from_value(value).map_err(E::custom)
940 }
941
942 match message_type {
943 "system" => parse(payload).map(Self::System),
944 "user" => parse(payload).map(Self::User),
945 "assistant" => parse(payload).map(Self::Assistant),
946 "result" if value.get("subtype").is_some() => parse(payload).map(Self::Result),
947 "result" => parse(payload).map(Self::TranscriptResult),
948 "control_request" => parse(payload).map(Self::ControlRequest),
949 "control_response" => parse(payload).map(Self::ControlResponse),
950 "control_cancel_request" => parse(payload).map(Self::ControlCancelRequest),
951 "error" => parse(payload).map(Self::Error),
952 "rate_limit_event" => parse(payload).map(Self::RateLimitEvent),
953 "stream_event" => parse(payload).map(Self::StreamEvent),
954 "tool_progress" => parse(payload).map(Self::ToolProgress),
955 "command_lifecycle" => parse(payload).map(Self::CommandLifecycle),
956 "auth_status" => parse(payload).map(Self::AuthStatus),
957 "tool_use_summary" => parse(payload).map(Self::ToolUseSummary),
958 "prompt_suggestion" => parse(payload).map(Self::PromptSuggestion),
959 "conversation_reset" => parse(payload).map(Self::ConversationReset),
960 "progress" => parse(payload).map(Self::Progress),
961 "queue-operation" => parse(payload).map(Self::QueueOperation),
962 "pr-link" => parse(payload).map(Self::PrLink),
963 "file-history-snapshot" => parse(payload).map(Self::FileHistorySnapshot),
964 "summary" => parse(payload).map(Self::Summary),
965 "mode" => parse(payload).map(Self::Mode),
966 "permission-mode" => parse(payload).map(Self::PermissionMode),
967 "attachment" => parse(payload).map(Self::Attachment),
968 "ai-title" => parse(payload).map(Self::AiTitle),
969 "last-prompt" => parse(payload).map(Self::LastPrompt),
970 "started" => parse(payload).map(Self::Started),
971 other => Err(serde::de::Error::unknown_variant(
972 other,
973 &[
974 "system",
975 "user",
976 "assistant",
977 "result",
978 "control_request",
979 "control_response",
980 "control_cancel_request",
981 "error",
982 "rate_limit_event",
983 "stream_event",
984 "tool_progress",
985 "command_lifecycle",
986 "auth_status",
987 "tool_use_summary",
988 "prompt_suggestion",
989 "conversation_reset",
990 "progress",
991 "queue-operation",
992 "pr-link",
993 "file-history-snapshot",
994 "summary",
995 "mode",
996 "permission-mode",
997 "attachment",
998 "ai-title",
999 "last-prompt",
1000 "started",
1001 ],
1002 )),
1003 }
1004 }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010
1011 #[test]
1012 fn test_deserialize_control_cancel_request() {
1013 let json = r#"{"request_id":"00aea85d-70f9-4cc4-becc-b0f05f5037f3","type":"control_cancel_request"}"#;
1014 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1015 assert!(output.is_control_cancel_request());
1016 assert_eq!(output.message_type(), "control_cancel_request");
1017 assert_eq!(output.session_id(), None);
1018 let cancel = output.as_control_cancel_request().unwrap();
1019 assert_eq!(cancel.request_id, "00aea85d-70f9-4cc4-becc-b0f05f5037f3");
1020
1021 let round_trip: Value = serde_json::to_value(&output).unwrap();
1022 assert_eq!(round_trip, serde_json::from_str::<Value>(json).unwrap());
1023
1024 let bridged = r#"{"type":"control_cancel_request","request_id":"r1","session_id":"s1"}"#;
1025 let output: ClaudeOutput = serde_json::from_str(bridged).unwrap();
1026 assert_eq!(output.session_id(), Some("s1"));
1027 }
1028
1029 #[test]
1030 fn test_deserialize_assistant_message() {
1031 let json = r#"{
1032 "type": "assistant",
1033 "message": {
1034 "id": "msg_123",
1035 "role": "assistant",
1036 "model": "claude-3-sonnet",
1037 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1038 },
1039 "session_id": "123"
1040 }"#;
1041
1042 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1043 assert!(output.is_assistant_message());
1044 }
1045
1046 #[test]
1047 fn test_deserialize_new_top_level_message_types() {
1048 let cases = [
1049 (
1050 r#"{"type":"stream_event","event":{"type":"content_block_delta"},"parent_tool_use_id":null,"uuid":"u1","session_id":"s1","ttft_ms":12}"#,
1051 "stream_event",
1052 ),
1053 (
1054 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"}"#,
1055 "tool_progress",
1056 ),
1057 (
1058 r#"{"type":"auth_status","isAuthenticating":true,"output":["login"],"uuid":"u3","session_id":"s3"}"#,
1059 "auth_status",
1060 ),
1061 (
1062 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"}"#,
1063 "tool_use_summary",
1064 ),
1065 (
1066 r#"{"type":"prompt_suggestion","suggestion":"Run tests","uuid":"u5","session_id":"s5"}"#,
1067 "prompt_suggestion",
1068 ),
1069 (
1070 r#"{"type":"conversation_reset","new_conversation_id":"new-session","uuid":"u6","session_id":"s6"}"#,
1071 "conversation_reset",
1072 ),
1073 (
1074 r#"{"type":"command_lifecycle","command_uuid":"cmd-1","state":"queued","uuid":"u7","session_id":"s7"}"#,
1075 "command_lifecycle",
1076 ),
1077 ];
1078
1079 for (json, message_type) in cases {
1080 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1081 assert_eq!(output.message_type(), message_type);
1082 assert!(output.session_id().is_some());
1083 }
1084 }
1085
1086 #[test]
1087 fn test_command_lifecycle_states_roundtrip() {
1088 for state in ["queued", "started", "completed", "cancelled", "discarded"] {
1089 let json = format!(
1090 r#"{{"type":"command_lifecycle","command_uuid":"cmd-1","state":"{}","uuid":"u1","session_id":"s1"}}"#,
1091 state
1092 );
1093 let output: ClaudeOutput = serde_json::from_str(&json).unwrap();
1094 let ClaudeOutput::CommandLifecycle(msg) = &output else {
1095 panic!("expected CommandLifecycle");
1096 };
1097 assert_eq!(msg.state.as_str(), state);
1098 assert!(!matches!(msg.state, CommandLifecycleState::Unknown(_)));
1099 assert_eq!(serde_json::to_string(&output).unwrap(), json);
1100 }
1101
1102 let json = r#"{"type":"command_lifecycle","command_uuid":"cmd-2","state":"parked","uuid":"u2","session_id":"s2"}"#;
1104 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1105 let ClaudeOutput::CommandLifecycle(msg) = &output else {
1106 panic!("expected CommandLifecycle");
1107 };
1108 assert_eq!(
1109 msg.state,
1110 CommandLifecycleState::Unknown("parked".to_string())
1111 );
1112 assert_eq!(serde_json::to_string(&output).unwrap(), json);
1113 }
1114
1115 #[test]
1116 fn test_tool_progress_heartbeat_and_subagent_retry() {
1117 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"}}"#;
1118 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1119 let ClaudeOutput::ToolProgress(msg) = &output else {
1120 panic!("expected ToolProgress");
1121 };
1122 assert_eq!(msg.heartbeat, Some(true));
1123 assert_eq!(msg.subagent_type.as_deref(), Some("Explore"));
1124 let retry = msg.subagent_retry.as_ref().unwrap();
1125 assert_eq!(retry.attempt, 2);
1126 assert_eq!(retry.error_status, Some(529));
1127 assert_eq!(retry.error_category, "overloaded");
1128
1129 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"}}"#;
1131 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1132 let ClaudeOutput::ToolProgress(msg) = &output else {
1133 panic!("expected ToolProgress");
1134 };
1135 assert_eq!(msg.subagent_retry.as_ref().unwrap().error_status, None);
1136 }
1137
1138 #[test]
1139 fn test_deserialize_transcript_only_message_types() {
1140 let cases = [
1141 (
1142 r#"{"type":"progress","sessionId":"s1","content":"delta"}"#,
1143 "progress",
1144 Some("s1"),
1145 ),
1146 (
1147 r#"{"type":"queue-operation","sessionId":"s2","operation":"push"}"#,
1148 "queue-operation",
1149 Some("s2"),
1150 ),
1151 (
1152 r#"{"type":"pr-link","sessionId":"s3","url":"https://example.invalid/pr"}"#,
1153 "pr-link",
1154 Some("s3"),
1155 ),
1156 (
1157 r#"{"type":"file-history-snapshot","sessionId":"s4","files":[]}"#,
1158 "file-history-snapshot",
1159 Some("s4"),
1160 ),
1161 (r#"{"type":"summary","summary":"Done"}"#, "summary", None),
1162 (
1163 r#"{"type":"mode","mode":"normal","sessionId":"s5"}"#,
1164 "mode",
1165 Some("s5"),
1166 ),
1167 (
1168 r#"{"type":"permission-mode","permissionMode":"default","sessionId":"s6"}"#,
1169 "permission-mode",
1170 Some("s6"),
1171 ),
1172 (
1173 r#"{"type":"attachment","attachment":{"type":"task_reminder"},"sessionId":"s7"}"#,
1174 "attachment",
1175 Some("s7"),
1176 ),
1177 (
1178 r#"{"type":"ai-title","aiTitle":"Title","sessionId":"s8"}"#,
1179 "ai-title",
1180 Some("s8"),
1181 ),
1182 (
1183 r#"{"type":"last-prompt","lastPrompt":"prompt","sessionId":"s9"}"#,
1184 "last-prompt",
1185 Some("s9"),
1186 ),
1187 (
1188 r#"{"type":"started","sessionId":"s10"}"#,
1189 "started",
1190 Some("s10"),
1191 ),
1192 (
1193 r#"{"type":"result","key":"k","agentId":"a","result":{"ok":true}}"#,
1194 "result",
1195 None,
1196 ),
1197 ];
1198
1199 for (json, message_type, session_id) in cases {
1200 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1201 assert_eq!(output.message_type(), message_type);
1202 assert_eq!(output.session_id(), session_id);
1203 }
1204 }
1205
1206 #[test]
1207 fn test_transcript_assistant_accepts_camel_case_session_id() {
1208 let json = r#"{
1209 "type": "assistant",
1210 "sessionId": "camel-session",
1211 "message": {
1212 "id": "msg_123",
1213 "role": "assistant",
1214 "model": "claude-3-sonnet",
1215 "content": [{"type": "text", "text": "Hello"}]
1216 }
1217 }"#;
1218
1219 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1220 assert_eq!(output.session_id(), Some("camel-session"));
1221 }
1222
1223 #[test]
1224 fn test_is_system_init() {
1225 let init_json = r#"{
1226 "type": "system",
1227 "subtype": "init",
1228 "session_id": "test-session"
1229 }"#;
1230 let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
1231 assert!(output.is_system_init());
1232
1233 let status_json = r#"{
1234 "type": "system",
1235 "subtype": "status",
1236 "session_id": "test-session"
1237 }"#;
1238 let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
1239 assert!(!output.is_system_init());
1240 }
1241
1242 #[test]
1243 fn test_session_id() {
1244 let result_json = r#"{
1246 "type": "result",
1247 "subtype": "success",
1248 "is_error": false,
1249 "duration_ms": 100,
1250 "duration_api_ms": 200,
1251 "num_turns": 1,
1252 "session_id": "result-session",
1253 "total_cost_usd": 0.01
1254 }"#;
1255 let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1256 assert_eq!(output.session_id(), Some("result-session"));
1257
1258 let assistant_json = r#"{
1260 "type": "assistant",
1261 "message": {
1262 "id": "msg_1",
1263 "role": "assistant",
1264 "model": "claude-3",
1265 "content": []
1266 },
1267 "session_id": "assistant-session"
1268 }"#;
1269 let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1270 assert_eq!(output.session_id(), Some("assistant-session"));
1271
1272 let system_json = r#"{
1274 "type": "system",
1275 "subtype": "init",
1276 "session_id": "system-session"
1277 }"#;
1278 let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
1279 assert_eq!(output.session_id(), Some("system-session"));
1280 }
1281
1282 #[test]
1283 fn test_as_tool_use() {
1284 let json = r#"{
1285 "type": "assistant",
1286 "message": {
1287 "id": "msg_1",
1288 "role": "assistant",
1289 "model": "claude-3",
1290 "content": [
1291 {"type": "text", "text": "Let me run that command."},
1292 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
1293 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
1294 ]
1295 },
1296 "session_id": "abc"
1297 }"#;
1298 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1299
1300 let bash = output.as_tool_use("Bash");
1302 assert!(bash.is_some());
1303 assert_eq!(bash.unwrap().id, "tu_1");
1304
1305 let read = output.as_tool_use("Read");
1307 assert!(read.is_some());
1308 assert_eq!(read.unwrap().id, "tu_2");
1309
1310 assert!(output.as_tool_use("Write").is_none());
1312
1313 let result_json = r#"{
1315 "type": "result",
1316 "subtype": "success",
1317 "is_error": false,
1318 "duration_ms": 100,
1319 "duration_api_ms": 200,
1320 "num_turns": 1,
1321 "session_id": "abc",
1322 "total_cost_usd": 0.01
1323 }"#;
1324 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1325 assert!(result.as_tool_use("Bash").is_none());
1326 }
1327
1328 #[test]
1329 fn test_tool_uses() {
1330 let json = r#"{
1331 "type": "assistant",
1332 "message": {
1333 "id": "msg_1",
1334 "role": "assistant",
1335 "model": "claude-3",
1336 "content": [
1337 {"type": "text", "text": "Running commands..."},
1338 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
1339 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
1340 {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
1341 ]
1342 },
1343 "session_id": "abc"
1344 }"#;
1345 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1346
1347 let tools: Vec<_> = output.tool_uses().collect();
1348 assert_eq!(tools.len(), 3);
1349 assert_eq!(tools[0].name, "Bash");
1350 assert_eq!(tools[1].name, "Read");
1351 assert_eq!(tools[2].name, "Write");
1352 }
1353
1354 #[test]
1355 fn test_text_content() {
1356 let json = r#"{
1358 "type": "assistant",
1359 "message": {
1360 "id": "msg_1",
1361 "role": "assistant",
1362 "model": "claude-3",
1363 "content": [{"type": "text", "text": "Hello, world!"}]
1364 },
1365 "session_id": "abc"
1366 }"#;
1367 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1368 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1369
1370 let json = r#"{
1372 "type": "assistant",
1373 "message": {
1374 "id": "msg_1",
1375 "role": "assistant",
1376 "model": "claude-3",
1377 "content": [
1378 {"type": "text", "text": "Hello, "},
1379 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
1380 {"type": "text", "text": "world!"}
1381 ]
1382 },
1383 "session_id": "abc"
1384 }"#;
1385 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1386 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1387
1388 let json = r#"{
1390 "type": "assistant",
1391 "message": {
1392 "id": "msg_1",
1393 "role": "assistant",
1394 "model": "claude-3",
1395 "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
1396 },
1397 "session_id": "abc"
1398 }"#;
1399 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1400 assert_eq!(output.text_content(), None);
1401
1402 let json = r#"{
1404 "type": "result",
1405 "subtype": "success",
1406 "is_error": false,
1407 "duration_ms": 100,
1408 "duration_api_ms": 200,
1409 "num_turns": 1,
1410 "session_id": "abc",
1411 "total_cost_usd": 0.01
1412 }"#;
1413 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1414 assert_eq!(output.text_content(), None);
1415 }
1416
1417 #[test]
1418 fn test_as_assistant() {
1419 let json = r#"{
1420 "type": "assistant",
1421 "message": {
1422 "id": "msg_1",
1423 "role": "assistant",
1424 "model": "claude-sonnet-4",
1425 "content": []
1426 },
1427 "session_id": "abc"
1428 }"#;
1429 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1430
1431 let assistant = output.as_assistant();
1432 assert!(assistant.is_some());
1433 assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
1434
1435 let result_json = r#"{
1437 "type": "result",
1438 "subtype": "success",
1439 "is_error": false,
1440 "duration_ms": 100,
1441 "duration_api_ms": 200,
1442 "num_turns": 1,
1443 "session_id": "abc",
1444 "total_cost_usd": 0.01
1445 }"#;
1446 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1447 assert!(result.as_assistant().is_none());
1448 }
1449
1450 #[test]
1451 fn test_as_result() {
1452 let json = r#"{
1453 "type": "result",
1454 "subtype": "success",
1455 "is_error": false,
1456 "duration_ms": 100,
1457 "duration_api_ms": 200,
1458 "num_turns": 5,
1459 "session_id": "abc",
1460 "total_cost_usd": 0.05
1461 }"#;
1462 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1463
1464 let result = output.as_result();
1465 assert!(result.is_some());
1466 assert_eq!(result.unwrap().num_turns, 5);
1467 assert_eq!(result.unwrap().total_cost_usd, 0.05);
1468
1469 let assistant_json = r#"{
1471 "type": "assistant",
1472 "message": {
1473 "id": "msg_1",
1474 "role": "assistant",
1475 "model": "claude-3",
1476 "content": []
1477 },
1478 "session_id": "abc"
1479 }"#;
1480 let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1481 assert!(assistant.as_result().is_none());
1482 }
1483
1484 #[test]
1485 fn test_as_system() {
1486 let json = r#"{
1487 "type": "system",
1488 "subtype": "init",
1489 "session_id": "abc",
1490 "model": "claude-3"
1491 }"#;
1492 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1493
1494 let system = output.as_system();
1495 assert!(system.is_some());
1496 assert!(system.unwrap().is_init());
1497
1498 let result_json = r#"{
1500 "type": "result",
1501 "subtype": "success",
1502 "is_error": false,
1503 "duration_ms": 100,
1504 "duration_api_ms": 200,
1505 "num_turns": 1,
1506 "session_id": "abc",
1507 "total_cost_usd": 0.01
1508 }"#;
1509 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1510 assert!(result.as_system().is_none());
1511 }
1512}