1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4use uuid::Uuid;
5
6use super::claude_output::ClaudeOutput;
7use super::content_blocks::{deserialize_content_blocks, ContentBlock};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum SystemSubtype {
16 Init,
17 Status,
18 CompactBoundary,
19 ThinkingTokens,
20 TaskStarted,
21 TaskProgress,
22 TaskUpdated,
23 TaskNotification,
24 ApiRetry,
25 ControlRequestProgress,
26 ModelRefusalFallback,
27 ModelRefusalNoFallback,
28 LocalCommandOutput,
29 HookStarted,
30 HookProgress,
31 HookResponse,
32 PluginInstall,
33 BackgroundTasksChanged,
34 SessionStateChanged,
35 WorkerShuttingDown,
36 CommandsChanged,
37 Notification,
38 FilesPersisted,
39 MemoryRecall,
40 ElicitationComplete,
41 PermissionDenied,
42 MirrorError,
43 Informational,
44 CodeChangePublished,
45 VcsStateChanged,
46 Unknown(String),
48}
49
50impl SystemSubtype {
51 pub fn as_str(&self) -> &str {
52 match self {
53 Self::Init => "init",
54 Self::Status => "status",
55 Self::CompactBoundary => "compact_boundary",
56 Self::ThinkingTokens => "thinking_tokens",
57 Self::TaskStarted => "task_started",
58 Self::TaskProgress => "task_progress",
59 Self::TaskUpdated => "task_updated",
60 Self::TaskNotification => "task_notification",
61 Self::ApiRetry => "api_retry",
62 Self::ControlRequestProgress => "control_request_progress",
63 Self::ModelRefusalFallback => "model_refusal_fallback",
64 Self::ModelRefusalNoFallback => "model_refusal_no_fallback",
65 Self::LocalCommandOutput => "local_command_output",
66 Self::HookStarted => "hook_started",
67 Self::HookProgress => "hook_progress",
68 Self::HookResponse => "hook_response",
69 Self::PluginInstall => "plugin_install",
70 Self::BackgroundTasksChanged => "background_tasks_changed",
71 Self::SessionStateChanged => "session_state_changed",
72 Self::WorkerShuttingDown => "worker_shutting_down",
73 Self::CommandsChanged => "commands_changed",
74 Self::Notification => "notification",
75 Self::FilesPersisted => "files_persisted",
76 Self::MemoryRecall => "memory_recall",
77 Self::ElicitationComplete => "elicitation_complete",
78 Self::PermissionDenied => "permission_denied",
79 Self::MirrorError => "mirror_error",
80 Self::Informational => "informational",
81 Self::CodeChangePublished => "code_change_published",
82 Self::VcsStateChanged => "vcs_state_changed",
83 Self::Unknown(s) => s.as_str(),
84 }
85 }
86}
87
88impl fmt::Display for SystemSubtype {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 f.write_str(self.as_str())
91 }
92}
93
94impl From<&str> for SystemSubtype {
95 fn from(s: &str) -> Self {
96 match s {
97 "init" => Self::Init,
98 "status" => Self::Status,
99 "compact_boundary" => Self::CompactBoundary,
100 "thinking_tokens" => Self::ThinkingTokens,
101 "task_started" => Self::TaskStarted,
102 "task_progress" => Self::TaskProgress,
103 "task_updated" => Self::TaskUpdated,
104 "task_notification" => Self::TaskNotification,
105 "api_retry" => Self::ApiRetry,
106 "control_request_progress" => Self::ControlRequestProgress,
107 "model_refusal_fallback" => Self::ModelRefusalFallback,
108 "model_refusal_no_fallback" => Self::ModelRefusalNoFallback,
109 "local_command_output" => Self::LocalCommandOutput,
110 "hook_started" => Self::HookStarted,
111 "hook_progress" => Self::HookProgress,
112 "hook_response" => Self::HookResponse,
113 "plugin_install" => Self::PluginInstall,
114 "background_tasks_changed" => Self::BackgroundTasksChanged,
115 "session_state_changed" => Self::SessionStateChanged,
116 "worker_shutting_down" => Self::WorkerShuttingDown,
117 "commands_changed" => Self::CommandsChanged,
118 "notification" => Self::Notification,
119 "files_persisted" => Self::FilesPersisted,
120 "memory_recall" => Self::MemoryRecall,
121 "elicitation_complete" => Self::ElicitationComplete,
122 "permission_denied" => Self::PermissionDenied,
123 "mirror_error" => Self::MirrorError,
124 "informational" => Self::Informational,
125 "code_change_published" => Self::CodeChangePublished,
126 "vcs_state_changed" => Self::VcsStateChanged,
127 other => Self::Unknown(other.to_string()),
128 }
129 }
130}
131
132impl Serialize for SystemSubtype {
133 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
134 serializer.serialize_str(self.as_str())
135 }
136}
137
138impl<'de> Deserialize<'de> for SystemSubtype {
139 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
140 let s = String::deserialize(deserializer)?;
141 Ok(Self::from(s.as_str()))
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Hash)]
150pub enum MessageRole {
151 User,
152 Assistant,
153 Unknown(String),
155}
156
157impl MessageRole {
158 pub fn as_str(&self) -> &str {
159 match self {
160 Self::User => "user",
161 Self::Assistant => "assistant",
162 Self::Unknown(s) => s.as_str(),
163 }
164 }
165}
166
167impl fmt::Display for MessageRole {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.write_str(self.as_str())
170 }
171}
172
173impl From<&str> for MessageRole {
174 fn from(s: &str) -> Self {
175 match s {
176 "user" => Self::User,
177 "assistant" => Self::Assistant,
178 other => Self::Unknown(other.to_string()),
179 }
180 }
181}
182
183impl Serialize for MessageRole {
184 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
185 serializer.serialize_str(self.as_str())
186 }
187}
188
189impl<'de> Deserialize<'de> for MessageRole {
190 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
191 let s = String::deserialize(deserializer)?;
192 Ok(Self::from(s.as_str()))
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Hash)]
198pub enum CompactionTrigger {
199 Auto,
201 Manual,
203 Unknown(String),
205}
206
207impl CompactionTrigger {
208 pub fn as_str(&self) -> &str {
209 match self {
210 Self::Auto => "auto",
211 Self::Manual => "manual",
212 Self::Unknown(s) => s.as_str(),
213 }
214 }
215}
216
217impl fmt::Display for CompactionTrigger {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 f.write_str(self.as_str())
220 }
221}
222
223impl From<&str> for CompactionTrigger {
224 fn from(s: &str) -> Self {
225 match s {
226 "auto" => Self::Auto,
227 "manual" => Self::Manual,
228 other => Self::Unknown(other.to_string()),
229 }
230 }
231}
232
233impl Serialize for CompactionTrigger {
234 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
235 serializer.serialize_str(self.as_str())
236 }
237}
238
239impl<'de> Deserialize<'de> for CompactionTrigger {
240 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
241 let s = String::deserialize(deserializer)?;
242 Ok(Self::from(s.as_str()))
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248pub enum StopReason {
249 EndTurn,
251 MaxTokens,
253 ToolUse,
255 Unknown(String),
257}
258
259impl StopReason {
260 pub fn as_str(&self) -> &str {
261 match self {
262 Self::EndTurn => "end_turn",
263 Self::MaxTokens => "max_tokens",
264 Self::ToolUse => "tool_use",
265 Self::Unknown(s) => s.as_str(),
266 }
267 }
268}
269
270impl fmt::Display for StopReason {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 f.write_str(self.as_str())
273 }
274}
275
276impl From<&str> for StopReason {
277 fn from(s: &str) -> Self {
278 match s {
279 "end_turn" => Self::EndTurn,
280 "max_tokens" => Self::MaxTokens,
281 "tool_use" => Self::ToolUse,
282 other => Self::Unknown(other.to_string()),
283 }
284 }
285}
286
287impl Serialize for StopReason {
288 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
289 serializer.serialize_str(self.as_str())
290 }
291}
292
293impl<'de> Deserialize<'de> for StopReason {
294 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
295 let s = String::deserialize(deserializer)?;
296 Ok(Self::from(s.as_str()))
297 }
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Hash)]
302pub enum ApiKeySource {
303 None,
305 User,
306 Project,
307 Org,
308 Temporary,
309 Oauth,
310 Unknown(String),
312}
313
314impl ApiKeySource {
315 pub fn as_str(&self) -> &str {
316 match self {
317 Self::None => "none",
318 Self::User => "user",
319 Self::Project => "project",
320 Self::Org => "org",
321 Self::Temporary => "temporary",
322 Self::Oauth => "oauth",
323 Self::Unknown(s) => s.as_str(),
324 }
325 }
326}
327
328impl fmt::Display for ApiKeySource {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 f.write_str(self.as_str())
331 }
332}
333
334impl From<&str> for ApiKeySource {
335 fn from(s: &str) -> Self {
336 match s {
337 "none" => Self::None,
338 "user" => Self::User,
339 "project" => Self::Project,
340 "org" => Self::Org,
341 "temporary" => Self::Temporary,
342 "oauth" => Self::Oauth,
343 other => Self::Unknown(other.to_string()),
344 }
345 }
346}
347
348impl Serialize for ApiKeySource {
349 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
350 serializer.serialize_str(self.as_str())
351 }
352}
353
354impl<'de> Deserialize<'de> for ApiKeySource {
355 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
356 let s = String::deserialize(deserializer)?;
357 Ok(Self::from(s.as_str()))
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Hash)]
363pub enum OutputStyle {
364 Default,
366 Unknown(String),
368}
369
370impl OutputStyle {
371 pub fn as_str(&self) -> &str {
372 match self {
373 Self::Default => "default",
374 Self::Unknown(s) => s.as_str(),
375 }
376 }
377}
378
379impl fmt::Display for OutputStyle {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 f.write_str(self.as_str())
382 }
383}
384
385impl From<&str> for OutputStyle {
386 fn from(s: &str) -> Self {
387 match s {
388 "default" => Self::Default,
389 other => Self::Unknown(other.to_string()),
390 }
391 }
392}
393
394impl Serialize for OutputStyle {
395 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
396 serializer.serialize_str(self.as_str())
397 }
398}
399
400impl<'de> Deserialize<'de> for OutputStyle {
401 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
402 let s = String::deserialize(deserializer)?;
403 Ok(Self::from(s.as_str()))
404 }
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Hash)]
409pub enum InitPermissionMode {
410 Default,
412 AcceptEdits,
413 BypassPermissions,
414 Plan,
415 DontAsk,
416 Auto,
417 Unknown(String),
419}
420
421impl InitPermissionMode {
422 pub fn as_str(&self) -> &str {
423 match self {
424 Self::Default => "default",
425 Self::AcceptEdits => "acceptEdits",
426 Self::BypassPermissions => "bypassPermissions",
427 Self::Plan => "plan",
428 Self::DontAsk => "dontAsk",
429 Self::Auto => "auto",
430 Self::Unknown(s) => s.as_str(),
431 }
432 }
433}
434
435impl fmt::Display for InitPermissionMode {
436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437 f.write_str(self.as_str())
438 }
439}
440
441impl From<&str> for InitPermissionMode {
442 fn from(s: &str) -> Self {
443 match s {
444 "default" => Self::Default,
445 "acceptEdits" => Self::AcceptEdits,
446 "bypassPermissions" => Self::BypassPermissions,
447 "plan" => Self::Plan,
448 "dontAsk" => Self::DontAsk,
449 "auto" => Self::Auto,
450 other => Self::Unknown(other.to_string()),
451 }
452 }
453}
454
455impl Serialize for InitPermissionMode {
456 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
457 serializer.serialize_str(self.as_str())
458 }
459}
460
461impl<'de> Deserialize<'de> for InitPermissionMode {
462 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
463 let s = String::deserialize(deserializer)?;
464 Ok(Self::from(s.as_str()))
465 }
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Hash)]
470pub enum StatusMessageStatus {
471 Compacting,
473 Requesting,
475 Unknown(String),
477}
478
479impl StatusMessageStatus {
480 pub fn as_str(&self) -> &str {
481 match self {
482 Self::Compacting => "compacting",
483 Self::Requesting => "requesting",
484 Self::Unknown(s) => s.as_str(),
485 }
486 }
487}
488
489impl fmt::Display for StatusMessageStatus {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 f.write_str(self.as_str())
492 }
493}
494
495impl From<&str> for StatusMessageStatus {
496 fn from(s: &str) -> Self {
497 match s {
498 "compacting" => Self::Compacting,
499 "requesting" => Self::Requesting,
500 other => Self::Unknown(other.to_string()),
501 }
502 }
503}
504
505impl Serialize for StatusMessageStatus {
506 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
507 serializer.serialize_str(self.as_str())
508 }
509}
510
511impl<'de> Deserialize<'de> for StatusMessageStatus {
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
518pub(crate) fn serialize_optional_uuid<S>(
520 uuid: &Option<Uuid>,
521 serializer: S,
522) -> Result<S::Ok, S::Error>
523where
524 S: Serializer,
525{
526 match uuid {
527 Some(id) => serializer.serialize_str(&id.to_string()),
528 None => serializer.serialize_none(),
529 }
530}
531
532pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
534where
535 D: Deserializer<'de>,
536{
537 let opt_str: Option<String> = Option::deserialize(deserializer)?;
538 match opt_str {
539 Some(s) => Uuid::parse_str(&s)
540 .map(Some)
541 .map_err(serde::de::Error::custom),
542 None => Ok(None),
543 }
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
549pub struct MessageOrigin {
550 pub kind: String,
551 #[serde(flatten)]
552 pub extra: serde_json::Map<String, Value>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
557pub struct SummarizeMetadata {
558 pub messages_summarized: u64,
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub user_context: Option<String>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub direction: Option<String>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
567pub struct McpMeta {
568 #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
569 pub meta: Option<Value>,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
571 pub structured_content: Option<Value>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
576pub struct ToolResultMeta {
577 pub id: String,
579 pub non_execution_kind: String,
585 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub user_feedback: Option<String>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct UserMessage {
593 pub message: MessageContent,
594 #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
595 #[serde(
596 serialize_with = "serialize_optional_uuid",
597 deserialize_with = "deserialize_optional_uuid"
598 )]
599 pub session_id: Option<Uuid>,
600 #[serde(skip_serializing_if = "Option::is_none")]
602 pub parent_tool_use_id: Option<String>,
603 #[serde(skip_serializing_if = "Option::is_none")]
605 pub uuid: Option<String>,
606 #[serde(skip_serializing_if = "Option::is_none")]
608 pub timestamp: Option<String>,
609 #[serde(skip_serializing_if = "Option::is_none")]
616 pub tool_use_result: Option<serde_json::Value>,
617 #[serde(skip_serializing_if = "Option::is_none")]
620 pub subagent_type: Option<String>,
621 #[serde(skip_serializing_if = "Option::is_none")]
623 pub task_description: Option<String>,
624 #[serde(skip_serializing_if = "Option::is_none")]
625 pub origin: Option<MessageOrigin>,
626 #[serde(skip_serializing_if = "Option::is_none")]
627 pub priority: Option<String>,
628 #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
629 pub is_synthetic: Option<bool>,
630 #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
631 pub should_query: Option<bool>,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub is_meta: Option<bool>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub is_visible_in_transcript_only: Option<bool>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub is_virtual: Option<bool>,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub is_compact_summary: Option<bool>,
640 #[serde(skip_serializing_if = "Option::is_none")]
641 pub summarize_metadata: Option<SummarizeMetadata>,
642 #[serde(skip_serializing_if = "Option::is_none")]
643 pub mcp_meta: Option<McpMeta>,
644 #[serde(skip_serializing_if = "Option::is_none")]
647 pub tool_result_meta: Option<Vec<ToolResultMeta>>,
648 #[serde(skip_serializing_if = "Option::is_none")]
649 pub source_tool_use_id: Option<String>,
650 #[serde(skip_serializing_if = "Option::is_none")]
651 pub source_tool_assistant_uuid: Option<String>,
652 #[serde(skip_serializing_if = "Option::is_none")]
653 pub image_paste_ids: Option<Vec<u64>>,
654 #[serde(skip_serializing_if = "Option::is_none")]
655 pub client_platform: Option<String>,
656 #[serde(skip_serializing_if = "Option::is_none")]
657 pub inbound_origin: Option<String>,
658 #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
659 pub is_replay: Option<bool>,
660 #[serde(skip_serializing_if = "Option::is_none")]
661 pub file_attachments: Option<Vec<Value>>,
662}
663
664impl UserMessage {
665 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
673 &self,
674 ) -> Option<Result<T, serde_json::Error>> {
675 self.tool_use_result
676 .as_ref()
677 .map(|v| serde_json::from_value(v.clone()))
678 }
679
680 pub fn subagent_result(&self) -> Option<SubagentResult> {
691 self.tool_use_result
692 .as_ref()
693 .and_then(|v| serde_json::from_value(v.clone()).ok())
694 }
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct SubagentResult {
709 #[serde(skip_serializing_if = "Option::is_none")]
711 pub status: Option<String>,
712 #[serde(skip_serializing_if = "Option::is_none")]
714 pub prompt: Option<String>,
715 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
717 pub agent_id: Option<String>,
718 #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
720 pub agent_type: Option<String>,
721 #[serde(
723 default,
724 deserialize_with = "deserialize_content_blocks",
725 skip_serializing_if = "Vec::is_empty"
726 )]
727 pub content: Vec<ContentBlock>,
728 #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
730 pub resolved_model: Option<String>,
731 #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
733 pub total_duration_ms: Option<u64>,
734 #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
736 pub total_tokens: Option<u64>,
737 #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
739 pub total_tool_use_count: Option<u64>,
740 #[serde(skip_serializing_if = "Option::is_none")]
742 pub usage: Option<super::result::UsageInfo>,
743 #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
745 pub tool_stats: Option<SubagentToolStats>,
746}
747
748#[derive(Debug, Clone, Default, Serialize, Deserialize)]
753#[serde(rename_all = "camelCase")]
754pub struct SubagentToolStats {
755 #[serde(default)]
756 pub read_count: u64,
757 #[serde(default)]
758 pub search_count: u64,
759 #[serde(default)]
760 pub bash_count: u64,
761 #[serde(default)]
762 pub edit_file_count: u64,
763 #[serde(default)]
764 pub lines_added: u64,
765 #[serde(default)]
766 pub lines_removed: u64,
767 #[serde(default)]
768 pub other_tool_count: u64,
769 #[serde(flatten)]
770 pub extra: serde_json::Map<String, Value>,
771}
772
773#[derive(Debug, Clone, Default, PartialEq, Eq)]
800pub struct SubagentUsageRollup {
801 pub subagent_tokens: u64,
804 pub agent_count: u32,
806 pub tool_uses: u64,
808 pub duration_ms: u64,
810 seen_agent_ids: std::collections::BTreeSet<String>,
811}
812
813impl SubagentUsageRollup {
814 pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
820 match output {
821 ClaudeOutput::User(user) => self.observe_user(user),
822 _ => false,
823 }
824 }
825
826 pub fn observe_user(&mut self, user: &UserMessage) -> bool {
833 let Some(result) = user.subagent_result() else {
834 return false;
835 };
836 if result.total_tokens.is_none() && result.agent_id.is_none() {
837 return false;
838 }
839 if let Some(agent_id) = &result.agent_id {
840 if !self.seen_agent_ids.insert(agent_id.clone()) {
841 return false;
842 }
843 }
844 self.agent_count += 1;
845 self.subagent_tokens += result.total_tokens.unwrap_or(0);
846 self.tool_uses += result.total_tool_use_count.unwrap_or(0);
847 self.duration_ms += result.total_duration_ms.unwrap_or(0);
848 true
849 }
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
854pub struct MessageContent {
855 pub role: MessageRole,
856 #[serde(deserialize_with = "deserialize_content_blocks")]
857 pub content: Vec<ContentBlock>,
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct SystemMessage {
863 pub subtype: SystemSubtype,
864 #[serde(flatten)]
865 pub data: Value, }
867
868impl SystemMessage {
869 pub fn is_init(&self) -> bool {
871 self.subtype == SystemSubtype::Init
872 }
873
874 pub fn is_status(&self) -> bool {
876 self.subtype == SystemSubtype::Status
877 }
878
879 pub fn is_compact_boundary(&self) -> bool {
881 self.subtype == SystemSubtype::CompactBoundary
882 }
883
884 pub fn as_init(&self) -> Option<InitMessage> {
886 if self.subtype != SystemSubtype::Init {
887 return None;
888 }
889 serde_json::from_value(self.data.clone()).ok()
890 }
891
892 pub fn as_status(&self) -> Option<StatusMessage> {
894 if self.subtype != SystemSubtype::Status {
895 return None;
896 }
897 serde_json::from_value(self.data.clone()).ok()
898 }
899
900 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
902 if self.subtype != SystemSubtype::CompactBoundary {
903 return None;
904 }
905 serde_json::from_value(self.data.clone()).ok()
906 }
907
908 pub fn is_task_started(&self) -> bool {
910 self.subtype == SystemSubtype::TaskStarted
911 }
912
913 pub fn is_task_progress(&self) -> bool {
915 self.subtype == SystemSubtype::TaskProgress
916 }
917
918 pub fn is_task_notification(&self) -> bool {
920 self.subtype == SystemSubtype::TaskNotification
921 }
922
923 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
925 if self.subtype != SystemSubtype::TaskStarted {
926 return None;
927 }
928 serde_json::from_value(self.data.clone()).ok()
929 }
930
931 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
933 if self.subtype != SystemSubtype::TaskProgress {
934 return None;
935 }
936 serde_json::from_value(self.data.clone()).ok()
937 }
938
939 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
941 if self.subtype != SystemSubtype::TaskNotification {
942 return None;
943 }
944 serde_json::from_value(self.data.clone()).ok()
945 }
946
947 pub fn is_task_updated(&self) -> bool {
949 self.subtype == SystemSubtype::TaskUpdated
950 }
951
952 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
954 if self.subtype != SystemSubtype::TaskUpdated {
955 return None;
956 }
957 serde_json::from_value(self.data.clone()).ok()
958 }
959
960 pub fn is_thinking_tokens(&self) -> bool {
962 self.subtype == SystemSubtype::ThinkingTokens
963 }
964
965 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
967 if self.subtype != SystemSubtype::ThinkingTokens {
968 return None;
969 }
970 serde_json::from_value(self.data.clone()).ok()
971 }
972
973 pub fn is_code_change_published(&self) -> bool {
975 self.subtype == SystemSubtype::CodeChangePublished
976 }
977
978 pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
980 if self.subtype != SystemSubtype::CodeChangePublished {
981 return None;
982 }
983 serde_json::from_value(self.data.clone()).ok()
984 }
985
986 pub fn is_vcs_state_changed(&self) -> bool {
988 self.subtype == SystemSubtype::VcsStateChanged
989 }
990
991 pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
993 if self.subtype != SystemSubtype::VcsStateChanged {
994 return None;
995 }
996 serde_json::from_value(self.data.clone()).ok()
997 }
998
999 pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
1001 macro_rules! parse {
1002 ($variant:ident, $ty:ty) => {
1003 serde_json::from_value::<$ty>(self.data.clone())
1004 .ok()
1005 .map(KnownSystemEvent::$variant)
1006 };
1007 }
1008
1009 match self.subtype {
1010 SystemSubtype::Init => parse!(Init, InitMessage),
1011 SystemSubtype::Status => parse!(Status, StatusMessage),
1012 SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
1013 SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
1014 SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
1015 SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
1016 SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
1017 SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
1018 SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
1019 SystemSubtype::ControlRequestProgress => {
1020 parse!(ControlRequestProgress, ControlRequestProgressMessage)
1021 }
1022 SystemSubtype::ModelRefusalFallback => {
1023 parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
1024 }
1025 SystemSubtype::ModelRefusalNoFallback => {
1026 parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
1027 }
1028 SystemSubtype::LocalCommandOutput => {
1029 parse!(LocalCommandOutput, LocalCommandOutputMessage)
1030 }
1031 SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
1032 SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
1033 SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
1034 SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
1035 SystemSubtype::BackgroundTasksChanged => {
1036 parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
1037 }
1038 SystemSubtype::SessionStateChanged => {
1039 parse!(SessionStateChanged, SessionStateChangedMessage)
1040 }
1041 SystemSubtype::WorkerShuttingDown => {
1042 parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
1043 }
1044 SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
1045 SystemSubtype::Notification => parse!(Notification, NotificationMessage),
1046 SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
1047 SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
1048 SystemSubtype::ElicitationComplete => {
1049 parse!(ElicitationComplete, ElicitationCompleteMessage)
1050 }
1051 SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1052 SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1053 SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1054 SystemSubtype::CodeChangePublished => {
1055 parse!(CodeChangePublished, CodeChangePublishedMessage)
1056 }
1057 SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
1058 SystemSubtype::Unknown(_) => None,
1059 }
1060 }
1061
1062 pub fn typed_value(&self) -> Option<Value> {
1071 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1072 parsed.and_then(|v| serde_json::to_value(v).ok())
1073 }
1074 match self.subtype {
1075 SystemSubtype::Init => reserialize(self.as_init()),
1076 SystemSubtype::Status => reserialize(self.as_status()),
1077 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1078 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1079 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1080 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1081 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1082 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1083 SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1084 SystemSubtype::ControlRequestProgress => {
1085 reserialize(parse_system::<ControlRequestProgressMessage>(self))
1086 }
1087 SystemSubtype::ModelRefusalFallback => {
1088 reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1089 }
1090 SystemSubtype::ModelRefusalNoFallback => {
1091 reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1092 }
1093 SystemSubtype::LocalCommandOutput => {
1094 reserialize(parse_system::<LocalCommandOutputMessage>(self))
1095 }
1096 SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1097 SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1098 SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1099 SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1100 SystemSubtype::BackgroundTasksChanged => {
1101 reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1102 }
1103 SystemSubtype::SessionStateChanged => {
1104 reserialize(parse_system::<SessionStateChangedMessage>(self))
1105 }
1106 SystemSubtype::WorkerShuttingDown => {
1107 reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1108 }
1109 SystemSubtype::CommandsChanged => {
1110 reserialize(parse_system::<CommandsChangedMessage>(self))
1111 }
1112 SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1113 SystemSubtype::FilesPersisted => {
1114 reserialize(parse_system::<FilesPersistedMessage>(self))
1115 }
1116 SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1117 SystemSubtype::ElicitationComplete => {
1118 reserialize(parse_system::<ElicitationCompleteMessage>(self))
1119 }
1120 SystemSubtype::PermissionDenied => {
1121 reserialize(parse_system::<PermissionDeniedMessage>(self))
1122 }
1123 SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1124 SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1125 SystemSubtype::CodeChangePublished => {
1126 reserialize(parse_system::<CodeChangePublishedMessage>(self))
1127 }
1128 SystemSubtype::VcsStateChanged => {
1129 reserialize(parse_system::<VcsStateChangedMessage>(self))
1130 }
1131 SystemSubtype::Unknown(_) => None,
1132 }
1133 }
1134}
1135
1136fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1137 serde_json::from_value(message.data.clone()).ok()
1138}
1139
1140#[derive(Debug, Clone, Serialize, Deserialize)]
1142pub enum KnownSystemEvent {
1143 Init(InitMessage),
1144 Status(StatusMessage),
1145 CompactBoundary(CompactBoundaryMessage),
1146 ThinkingTokens(ThinkingTokensMessage),
1147 TaskStarted(TaskStartedMessage),
1148 TaskProgress(TaskProgressMessage),
1149 TaskUpdated(TaskUpdatedMessage),
1150 TaskNotification(TaskNotificationMessage),
1151 ApiRetry(ApiRetryMessage),
1152 ControlRequestProgress(ControlRequestProgressMessage),
1153 ModelRefusalFallback(ModelRefusalFallbackMessage),
1154 ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1155 LocalCommandOutput(LocalCommandOutputMessage),
1156 HookStarted(HookStartedMessage),
1157 HookProgress(HookProgressMessage),
1158 HookResponse(HookResponseMessage),
1159 PluginInstall(PluginInstallMessage),
1160 BackgroundTasksChanged(BackgroundTasksChangedMessage),
1161 SessionStateChanged(SessionStateChangedMessage),
1162 WorkerShuttingDown(WorkerShuttingDownMessage),
1163 CommandsChanged(CommandsChangedMessage),
1164 Notification(NotificationMessage),
1165 FilesPersisted(FilesPersistedMessage),
1166 MemoryRecall(MemoryRecallMessage),
1167 ElicitationComplete(ElicitationCompleteMessage),
1168 PermissionDenied(PermissionDeniedMessage),
1169 MirrorError(MirrorErrorMessage),
1170 Informational(InformationalMessage),
1171 CodeChangePublished(CodeChangePublishedMessage),
1172 VcsStateChanged(VcsStateChangedMessage),
1173}
1174
1175#[derive(Debug, Clone, Serialize, Deserialize)]
1176pub struct ApiRetryMessage {
1177 pub attempt: u64,
1178 pub max_retries: u64,
1179 pub retry_delay_ms: u64,
1180 pub error_status: Option<u16>,
1181 pub error: String,
1182 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub uuid: Option<String>,
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1185 pub session_id: Option<String>,
1186}
1187
1188#[derive(Debug, Clone, Serialize, Deserialize)]
1189pub struct ControlRequestProgressMessage {
1190 pub request_id: String,
1191 pub status: String,
1192 #[serde(default, skip_serializing_if = "Option::is_none")]
1193 pub attempt: Option<u64>,
1194 #[serde(default, skip_serializing_if = "Option::is_none")]
1195 pub max_retries: Option<u64>,
1196 #[serde(default, skip_serializing_if = "Option::is_none")]
1197 pub retry_delay_ms: Option<u64>,
1198 #[serde(default, skip_serializing_if = "Option::is_none")]
1199 pub error_status: Option<u16>,
1200 #[serde(default, skip_serializing_if = "Option::is_none")]
1201 pub error: Option<String>,
1202 #[serde(default, skip_serializing_if = "Option::is_none")]
1203 pub uuid: Option<String>,
1204 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub session_id: Option<String>,
1206}
1207
1208#[derive(Debug, Clone, Serialize, Deserialize)]
1209pub struct ModelRefusalFallbackMessage {
1210 pub trigger: String,
1211 pub direction: String,
1212 #[serde(default, skip_serializing_if = "Option::is_none")]
1218 pub scope: Option<RefusalFallbackScope>,
1219 pub original_model: String,
1220 pub fallback_model: String,
1221 pub request_id: Option<String>,
1222 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub api_refusal_category: Option<String>,
1224 #[serde(default, skip_serializing_if = "Option::is_none")]
1225 pub api_refusal_explanation: Option<String>,
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1227 pub retracted_message_uuids: Option<Vec<String>>,
1228 #[serde(default, skip_serializing_if = "Option::is_none")]
1229 pub refused_user_message_uuid: Option<String>,
1230 pub content: Value,
1231 #[serde(default, skip_serializing_if = "Option::is_none")]
1232 pub uuid: Option<String>,
1233 #[serde(default, skip_serializing_if = "Option::is_none")]
1234 pub session_id: Option<String>,
1235}
1236
1237#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1241pub enum RefusalFallbackScope {
1242 Session,
1244 Local,
1247 Unknown(String),
1249}
1250
1251impl RefusalFallbackScope {
1252 pub fn as_str(&self) -> &str {
1253 match self {
1254 Self::Session => "session",
1255 Self::Local => "local",
1256 Self::Unknown(s) => s.as_str(),
1257 }
1258 }
1259}
1260
1261impl fmt::Display for RefusalFallbackScope {
1262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1263 f.write_str(self.as_str())
1264 }
1265}
1266
1267impl From<&str> for RefusalFallbackScope {
1268 fn from(s: &str) -> Self {
1269 match s {
1270 "session" => Self::Session,
1271 "local" => Self::Local,
1272 other => Self::Unknown(other.to_string()),
1273 }
1274 }
1275}
1276
1277impl Serialize for RefusalFallbackScope {
1278 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1279 serializer.serialize_str(self.as_str())
1280 }
1281}
1282
1283impl<'de> Deserialize<'de> for RefusalFallbackScope {
1284 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1285 let s = String::deserialize(deserializer)?;
1286 Ok(Self::from(s.as_str()))
1287 }
1288}
1289
1290#[derive(Debug, Clone, Serialize, Deserialize)]
1291pub struct ModelRefusalNoFallbackMessage {
1292 pub original_model: String,
1293 pub request_id: Option<String>,
1294 #[serde(default, skip_serializing_if = "Option::is_none")]
1295 pub api_refusal_category: Option<String>,
1296 #[serde(default, skip_serializing_if = "Option::is_none")]
1297 pub api_refusal_explanation: Option<String>,
1298 pub content: Value,
1299 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub uuid: Option<String>,
1301 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub session_id: Option<String>,
1303}
1304
1305#[derive(Debug, Clone, Serialize, Deserialize)]
1306pub struct LocalCommandOutputMessage {
1307 pub content: String,
1308 #[serde(default, skip_serializing_if = "Option::is_none")]
1309 pub uuid: Option<String>,
1310 #[serde(default, skip_serializing_if = "Option::is_none")]
1311 pub session_id: Option<String>,
1312}
1313
1314#[derive(Debug, Clone, Serialize, Deserialize)]
1315pub struct HookStartedMessage {
1316 pub hook_id: String,
1317 pub hook_name: String,
1318 pub hook_event: String,
1319 #[serde(default, skip_serializing_if = "Option::is_none")]
1320 pub uuid: Option<String>,
1321 #[serde(default, skip_serializing_if = "Option::is_none")]
1322 pub session_id: Option<String>,
1323}
1324
1325#[derive(Debug, Clone, Serialize, Deserialize)]
1326pub struct HookProgressMessage {
1327 pub hook_id: String,
1328 pub hook_name: String,
1329 pub hook_event: String,
1330 #[serde(default, skip_serializing_if = "Option::is_none")]
1331 pub stdout: Option<String>,
1332 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub stderr: Option<String>,
1334 #[serde(default, skip_serializing_if = "Option::is_none")]
1335 pub output: Option<String>,
1336 #[serde(default, skip_serializing_if = "Option::is_none")]
1337 pub uuid: Option<String>,
1338 #[serde(default, skip_serializing_if = "Option::is_none")]
1339 pub session_id: Option<String>,
1340}
1341
1342#[derive(Debug, Clone, Serialize, Deserialize)]
1343pub struct HookResponseMessage {
1344 pub hook_id: String,
1345 pub hook_name: String,
1346 pub hook_event: String,
1347 #[serde(default, skip_serializing_if = "Option::is_none")]
1348 pub stdout: Option<String>,
1349 #[serde(default, skip_serializing_if = "Option::is_none")]
1350 pub stderr: Option<String>,
1351 #[serde(default, skip_serializing_if = "Option::is_none")]
1352 pub output: Option<String>,
1353 #[serde(default, skip_serializing_if = "Option::is_none")]
1354 pub exit_code: Option<i32>,
1355 pub outcome: String,
1356 #[serde(default, skip_serializing_if = "Option::is_none")]
1357 pub uuid: Option<String>,
1358 #[serde(default, skip_serializing_if = "Option::is_none")]
1359 pub session_id: Option<String>,
1360}
1361
1362#[derive(Debug, Clone, Serialize, Deserialize)]
1363pub struct PluginInstallMessage {
1364 pub status: String,
1365 #[serde(default, skip_serializing_if = "Option::is_none")]
1366 pub name: Option<String>,
1367 #[serde(default, skip_serializing_if = "Option::is_none")]
1368 pub error: Option<String>,
1369 #[serde(default, skip_serializing_if = "Option::is_none")]
1370 pub uuid: Option<String>,
1371 #[serde(default, skip_serializing_if = "Option::is_none")]
1372 pub session_id: Option<String>,
1373}
1374
1375#[derive(Debug, Clone, Serialize, Deserialize)]
1376pub struct BackgroundTasksChangedMessage {
1377 pub tasks: Vec<BackgroundTaskInfo>,
1378 #[serde(default, skip_serializing_if = "Option::is_none")]
1379 pub uuid: Option<String>,
1380 #[serde(default, skip_serializing_if = "Option::is_none")]
1381 pub session_id: Option<String>,
1382}
1383
1384#[derive(Debug, Clone, Serialize, Deserialize)]
1385pub struct BackgroundTaskInfo {
1386 pub task_id: String,
1387 pub task_type: String,
1388 pub description: String,
1389}
1390
1391#[derive(Debug, Clone, Serialize, Deserialize)]
1392pub struct SessionStateChangedMessage {
1393 pub state: String,
1394 #[serde(default, skip_serializing_if = "Option::is_none")]
1395 pub uuid: Option<String>,
1396 #[serde(default, skip_serializing_if = "Option::is_none")]
1397 pub session_id: Option<String>,
1398}
1399
1400#[derive(Debug, Clone, Serialize, Deserialize)]
1401pub struct WorkerShuttingDownMessage {
1402 pub reason: String,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1404 pub uuid: Option<String>,
1405 #[serde(default, skip_serializing_if = "Option::is_none")]
1406 pub session_id: Option<String>,
1407}
1408
1409#[derive(Debug, Clone, Serialize, Deserialize)]
1410pub struct CommandsChangedMessage {
1411 pub commands: Vec<CommandInfo>,
1412 #[serde(default, skip_serializing_if = "Option::is_none")]
1413 pub uuid: Option<String>,
1414 #[serde(default, skip_serializing_if = "Option::is_none")]
1415 pub session_id: Option<String>,
1416}
1417
1418#[derive(Debug, Clone, Serialize, Deserialize)]
1419pub struct CommandInfo {
1420 pub name: String,
1421 pub description: String,
1422 #[serde(rename = "argumentHint")]
1423 pub argument_hint: String,
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1425 pub aliases: Option<Vec<String>>,
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct NotificationMessage {
1430 pub key: String,
1431 pub text: String,
1432 pub priority: String,
1433 #[serde(default, skip_serializing_if = "Option::is_none")]
1434 pub color: Option<String>,
1435 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 pub timeout_ms: Option<u64>,
1437 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub uuid: Option<String>,
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1440 pub session_id: Option<String>,
1441}
1442
1443#[derive(Debug, Clone, Serialize, Deserialize)]
1444pub struct FilesPersistedMessage {
1445 pub files: Vec<PersistedFile>,
1446 pub failed: Vec<FailedPersistedFile>,
1447 pub processed_at: String,
1448 #[serde(default, skip_serializing_if = "Option::is_none")]
1449 pub uuid: Option<String>,
1450 #[serde(default, skip_serializing_if = "Option::is_none")]
1451 pub session_id: Option<String>,
1452}
1453
1454#[derive(Debug, Clone, Serialize, Deserialize)]
1455pub struct PersistedFile {
1456 pub filename: String,
1457 pub file_id: String,
1458}
1459
1460#[derive(Debug, Clone, Serialize, Deserialize)]
1461pub struct FailedPersistedFile {
1462 pub filename: String,
1463 pub error: String,
1464}
1465
1466#[derive(Debug, Clone, Serialize, Deserialize)]
1467pub struct MemoryRecallMessage {
1468 pub mode: String,
1469 pub memories: Vec<MemoryRecallItem>,
1470 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub uuid: Option<String>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub session_id: Option<String>,
1474}
1475
1476#[derive(Debug, Clone, Serialize, Deserialize)]
1477pub struct MemoryRecallItem {
1478 pub path: String,
1479 pub scope: String,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub content: Option<String>,
1482}
1483
1484#[derive(Debug, Clone, Serialize, Deserialize)]
1485pub struct ElicitationCompleteMessage {
1486 pub mcp_server_name: String,
1487 pub elicitation_id: String,
1488 #[serde(default, skip_serializing_if = "Option::is_none")]
1489 pub uuid: Option<String>,
1490 #[serde(default, skip_serializing_if = "Option::is_none")]
1491 pub session_id: Option<String>,
1492}
1493
1494#[derive(Debug, Clone, Serialize, Deserialize)]
1495pub struct PermissionDeniedMessage {
1496 pub tool_name: String,
1497 pub tool_use_id: String,
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1499 pub agent_id: Option<String>,
1500 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub decision_reason_type: Option<String>,
1502 #[serde(default, skip_serializing_if = "Option::is_none")]
1503 pub decision_reason: Option<String>,
1504 pub message: String,
1505 #[serde(default, skip_serializing_if = "Option::is_none")]
1506 pub uuid: Option<String>,
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1508 pub session_id: Option<String>,
1509}
1510
1511#[derive(Debug, Clone, Serialize, Deserialize)]
1512pub struct MirrorErrorMessage {
1513 pub error: String,
1514 pub key: MirrorErrorKey,
1515 #[serde(default, skip_serializing_if = "Option::is_none")]
1516 pub uuid: Option<String>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1518 pub session_id: Option<String>,
1519}
1520
1521#[derive(Debug, Clone, Serialize, Deserialize)]
1522pub struct MirrorErrorKey {
1523 #[serde(rename = "projectKey")]
1524 pub project_key: String,
1525 #[serde(rename = "sessionId")]
1526 pub session_id: String,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1528 pub subpath: Option<String>,
1529}
1530
1531#[derive(Debug, Clone, Serialize, Deserialize)]
1532pub struct InformationalMessage {
1533 pub content: String,
1534 pub level: String,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub tool_use_id: Option<String>,
1537 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub prevent_continuation: Option<bool>,
1539 #[serde(default, skip_serializing_if = "Option::is_none")]
1540 pub uuid: Option<String>,
1541 #[serde(default, skip_serializing_if = "Option::is_none")]
1542 pub session_id: Option<String>,
1543}
1544
1545#[derive(Debug, Clone, Serialize, Deserialize)]
1547pub struct PluginInfo {
1548 pub name: String,
1550 pub path: String,
1552 #[serde(skip_serializing_if = "Option::is_none")]
1554 pub source: Option<String>,
1555 #[serde(default, skip_serializing_if = "Option::is_none")]
1557 pub version: Option<String>,
1558}
1559
1560#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1562pub struct PluginDiagnostic {
1563 pub plugin: String,
1564 #[serde(rename = "type")]
1565 pub diagnostic_type: String,
1566 pub message: String,
1567}
1568
1569#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1571pub struct MemoryPaths {
1572 #[serde(default, skip_serializing_if = "Option::is_none")]
1573 pub auto: Option<String>,
1574 #[serde(default, skip_serializing_if = "Option::is_none")]
1575 pub team: Option<String>,
1576 #[serde(flatten)]
1577 pub extra: serde_json::Map<String, Value>,
1578}
1579
1580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1584pub struct McpServerError {
1585 pub name: String,
1586 #[serde(rename = "type")]
1588 pub error_type: String,
1589 pub message: String,
1590}
1591
1592#[derive(Debug, Clone, Serialize, Deserialize)]
1594pub struct InitMessage {
1595 pub session_id: String,
1597 #[serde(skip_serializing_if = "Option::is_none")]
1599 pub cwd: Option<String>,
1600 #[serde(skip_serializing_if = "Option::is_none")]
1602 pub model: Option<String>,
1603 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1605 pub tools: Vec<String>,
1606 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1608 pub mcp_servers: Vec<Value>,
1609 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1611 pub slash_commands: Vec<String>,
1612 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1614 pub agents: Vec<String>,
1615 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1617 pub plugins: Vec<PluginInfo>,
1618 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1620 pub skills: Vec<Value>,
1621 #[serde(skip_serializing_if = "Option::is_none")]
1623 pub claude_code_version: Option<String>,
1624 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1626 pub api_key_source: Option<ApiKeySource>,
1627 #[serde(skip_serializing_if = "Option::is_none")]
1629 pub output_style: Option<OutputStyle>,
1630 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1632 pub permission_mode: Option<InitPermissionMode>,
1633
1634 #[serde(skip_serializing_if = "Option::is_none")]
1636 pub uuid: Option<String>,
1637
1638 #[serde(skip_serializing_if = "Option::is_none")]
1640 pub memory_paths: Option<MemoryPaths>,
1641
1642 #[serde(skip_serializing_if = "Option::is_none")]
1644 pub fast_mode_state: Option<String>,
1645
1646 #[serde(default, skip_serializing_if = "Option::is_none")]
1648 pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
1649
1650 #[serde(default, skip_serializing_if = "Option::is_none")]
1653 pub mcp_server_errors: Option<Vec<McpServerError>>,
1654
1655 #[serde(default, skip_serializing_if = "Option::is_none")]
1657 pub analytics_disabled: Option<bool>,
1658
1659 #[serde(default, skip_serializing_if = "Option::is_none")]
1661 pub product_feedback_disabled: Option<bool>,
1662
1663 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1665 pub betas: Vec<String>,
1666
1667 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1669 pub capabilities: Vec<String>,
1670
1671 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1673 pub plugin_errors: Vec<PluginDiagnostic>,
1674
1675 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1677 pub plugin_warnings: Vec<PluginDiagnostic>,
1678}
1679
1680#[derive(Debug, Clone, Serialize, Deserialize)]
1682pub struct StatusMessage {
1683 pub session_id: String,
1685 pub status: Option<StatusMessageStatus>,
1687 #[serde(skip_serializing_if = "Option::is_none")]
1689 pub uuid: Option<String>,
1690 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1692 pub permission_mode: Option<InitPermissionMode>,
1693 #[serde(skip_serializing_if = "Option::is_none")]
1694 pub compact_result: Option<String>,
1695 #[serde(skip_serializing_if = "Option::is_none")]
1696 pub compact_error: Option<String>,
1697}
1698
1699#[derive(Debug, Clone, Serialize, Deserialize)]
1701pub struct CompactBoundaryMessage {
1702 pub session_id: String,
1704 pub compact_metadata: CompactMetadata,
1706 #[serde(
1710 default,
1711 skip_serializing_if = "Option::is_none",
1712 alias = "content",
1713 alias = "text"
1714 )]
1715 pub summary: Option<String>,
1716 #[serde(
1720 default,
1721 skip_serializing_if = "Option::is_none",
1722 alias = "message_count"
1723 )]
1724 pub leaf_message_count: Option<u32>,
1725 #[serde(default, skip_serializing_if = "Option::is_none")]
1727 pub duration_ms: Option<u64>,
1728 #[serde(skip_serializing_if = "Option::is_none")]
1730 pub uuid: Option<String>,
1731 #[serde(skip_serializing_if = "Option::is_none")]
1733 pub logical_parent_uuid: Option<Option<String>>,
1734}
1735
1736#[derive(Debug, Clone, Serialize, Deserialize)]
1738pub struct CompactMetadata {
1739 pub pre_tokens: u64,
1741 pub trigger: CompactionTrigger,
1743 #[serde(default, skip_serializing_if = "Option::is_none")]
1744 pub post_tokens: Option<u64>,
1745 #[serde(default, skip_serializing_if = "Option::is_none")]
1746 pub cumulative_dropped_tokens: Option<u64>,
1747 #[serde(default, skip_serializing_if = "Option::is_none")]
1748 pub duration_ms: Option<u64>,
1749 #[serde(default, skip_serializing_if = "Option::is_none")]
1750 pub user_context: Option<String>,
1751 #[serde(default, skip_serializing_if = "Option::is_none")]
1752 pub messages_summarized: Option<u64>,
1753 #[serde(default, skip_serializing_if = "Option::is_none")]
1754 pub precomputed: Option<bool>,
1755 #[serde(default, skip_serializing_if = "Option::is_none")]
1756 pub pre_compact_discovered_tools: Option<Vec<String>>,
1757 #[serde(default, skip_serializing_if = "Option::is_none")]
1758 pub preserved_segment: Option<PreservedSegment>,
1759 #[serde(default, skip_serializing_if = "Option::is_none")]
1760 pub preserved_messages: Option<PreservedMessages>,
1761}
1762
1763#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1764pub struct PreservedSegment {
1765 pub head_uuid: String,
1766 pub anchor_uuid: String,
1767 pub tail_uuid: String,
1768}
1769
1770#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1771pub struct PreservedMessages {
1772 pub anchor_uuid: String,
1773 pub uuids: Vec<String>,
1774 #[serde(default, skip_serializing_if = "Option::is_none")]
1775 pub all_uuids: Option<Vec<String>>,
1776}
1777
1778#[derive(Debug, Clone, Serialize, Deserialize)]
1784pub struct TaskUsage {
1785 pub duration_ms: u64,
1787 pub tool_uses: u64,
1789 pub total_tokens: u64,
1791}
1792
1793#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1795pub enum TaskType {
1796 LocalAgent,
1798 LocalBash,
1800 LocalWorkflow,
1802 Unknown(String),
1804}
1805
1806impl TaskType {
1807 pub fn as_str(&self) -> &str {
1808 match self {
1809 Self::LocalAgent => "local_agent",
1810 Self::LocalBash => "local_bash",
1811 Self::LocalWorkflow => "local_workflow",
1812 Self::Unknown(s) => s.as_str(),
1813 }
1814 }
1815}
1816
1817impl fmt::Display for TaskType {
1818 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1819 f.write_str(self.as_str())
1820 }
1821}
1822
1823impl From<&str> for TaskType {
1824 fn from(s: &str) -> Self {
1825 match s {
1826 "local_agent" => Self::LocalAgent,
1827 "local_bash" => Self::LocalBash,
1828 "local_workflow" => Self::LocalWorkflow,
1829 other => Self::Unknown(other.to_string()),
1830 }
1831 }
1832}
1833
1834impl Serialize for TaskType {
1835 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1836 serializer.serialize_str(self.as_str())
1837 }
1838}
1839
1840impl<'de> Deserialize<'de> for TaskType {
1841 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1842 let s = String::deserialize(deserializer)?;
1843 Ok(Self::from(s.as_str()))
1844 }
1845}
1846
1847#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1849pub enum TaskStatus {
1850 Pending,
1851 Running,
1852 Completed,
1853 Failed,
1854 Killed,
1855 Paused,
1856 Stopped,
1857 Unknown(String),
1858}
1859
1860impl TaskStatus {
1861 pub fn as_str(&self) -> &str {
1862 match self {
1863 Self::Pending => "pending",
1864 Self::Running => "running",
1865 Self::Completed => "completed",
1866 Self::Failed => "failed",
1867 Self::Killed => "killed",
1868 Self::Paused => "paused",
1869 Self::Stopped => "stopped",
1870 Self::Unknown(s) => s.as_str(),
1871 }
1872 }
1873}
1874
1875impl fmt::Display for TaskStatus {
1876 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1877 f.write_str(self.as_str())
1878 }
1879}
1880
1881impl From<&str> for TaskStatus {
1882 fn from(s: &str) -> Self {
1883 match s {
1884 "pending" => Self::Pending,
1885 "running" => Self::Running,
1886 "completed" => Self::Completed,
1887 "failed" => Self::Failed,
1888 "killed" => Self::Killed,
1889 "paused" => Self::Paused,
1890 "stopped" => Self::Stopped,
1891 other => Self::Unknown(other.to_string()),
1892 }
1893 }
1894}
1895
1896impl Serialize for TaskStatus {
1897 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1898 serializer.serialize_str(self.as_str())
1899 }
1900}
1901
1902impl<'de> Deserialize<'de> for TaskStatus {
1903 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1904 let s = String::deserialize(deserializer)?;
1905 Ok(Self::from(s.as_str()))
1906 }
1907}
1908
1909#[derive(Debug, Clone, Serialize, Deserialize)]
1911pub struct TaskStartedMessage {
1912 pub session_id: String,
1913 pub task_id: String,
1914 #[serde(default, skip_serializing_if = "Option::is_none")]
1915 pub task_type: Option<TaskType>,
1916 #[serde(default, skip_serializing_if = "Option::is_none")]
1917 pub tool_use_id: Option<String>,
1918 pub description: String,
1919 #[serde(default, skip_serializing_if = "Option::is_none")]
1922 pub subagent_type: Option<String>,
1923 #[serde(default, skip_serializing_if = "Option::is_none")]
1925 pub prompt: Option<String>,
1926 #[serde(default, skip_serializing_if = "Option::is_none")]
1927 pub workflow_name: Option<String>,
1928 #[serde(default, skip_serializing_if = "Option::is_none")]
1929 pub skip_transcript: Option<bool>,
1930 pub uuid: String,
1931}
1932
1933#[derive(Debug, Clone, Serialize, Deserialize)]
1937pub struct TaskUpdatedMessage {
1938 pub session_id: String,
1939 pub task_id: String,
1940 pub patch: TaskPatch,
1941 pub uuid: String,
1942}
1943
1944#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1947pub struct TaskPatch {
1948 #[serde(default, skip_serializing_if = "Option::is_none")]
1949 pub status: Option<TaskStatus>,
1950 #[serde(default, skip_serializing_if = "Option::is_none")]
1953 pub end_time: Option<u64>,
1954 #[serde(default, skip_serializing_if = "Option::is_none")]
1955 pub description: Option<String>,
1956 #[serde(default, skip_serializing_if = "Option::is_none")]
1957 pub total_paused_ms: Option<u64>,
1958 #[serde(default, skip_serializing_if = "Option::is_none")]
1959 pub error: Option<String>,
1960 #[serde(default, skip_serializing_if = "Option::is_none")]
1961 pub is_backgrounded: Option<bool>,
1962}
1963
1964#[derive(Debug, Clone, Serialize, Deserialize)]
1967pub struct ThinkingTokensMessage {
1968 pub session_id: String,
1969 pub estimated_tokens: u64,
1971 pub estimated_tokens_delta: u64,
1973 pub uuid: String,
1974}
1975
1976#[derive(Debug, Clone, Serialize, Deserialize)]
1979pub struct TaskProgressMessage {
1980 pub session_id: String,
1981 pub task_id: String,
1982 #[serde(default, skip_serializing_if = "Option::is_none")]
1983 pub tool_use_id: Option<String>,
1984 pub description: String,
1985 #[serde(default, skip_serializing_if = "Option::is_none")]
1986 pub last_tool_name: Option<String>,
1987 pub usage: TaskUsage,
1988 #[serde(default, skip_serializing_if = "Option::is_none")]
1990 pub subagent_type: Option<String>,
1991 #[serde(default, skip_serializing_if = "Option::is_none")]
1992 pub summary: Option<String>,
1993 pub uuid: String,
1994}
1995
1996#[derive(Debug, Clone, Serialize, Deserialize)]
1999pub struct TaskNotificationMessage {
2000 pub session_id: String,
2001 pub task_id: String,
2002 pub status: TaskStatus,
2003 pub summary: String,
2004 pub output_file: Option<String>,
2005 #[serde(skip_serializing_if = "Option::is_none")]
2006 pub tool_use_id: Option<String>,
2007 #[serde(skip_serializing_if = "Option::is_none")]
2008 pub usage: Option<TaskUsage>,
2009 #[serde(default, skip_serializing_if = "Option::is_none")]
2010 pub skip_transcript: Option<bool>,
2011 #[serde(skip_serializing_if = "Option::is_none")]
2012 pub uuid: Option<String>,
2013}
2014
2015#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2017pub enum AssistantErrorKind {
2018 AuthenticationFailed,
2019 OauthOrgNotAllowed,
2020 BillingError,
2021 RateLimit,
2022 Overloaded,
2023 InvalidRequest,
2024 ModelNotFound,
2025 ServerError,
2026 UnknownError,
2027 MaxOutputTokens,
2028 Unknown(String),
2029}
2030
2031impl AssistantErrorKind {
2032 pub fn as_str(&self) -> &str {
2033 match self {
2034 Self::AuthenticationFailed => "authentication_failed",
2035 Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
2036 Self::BillingError => "billing_error",
2037 Self::RateLimit => "rate_limit",
2038 Self::Overloaded => "overloaded",
2039 Self::InvalidRequest => "invalid_request",
2040 Self::ModelNotFound => "model_not_found",
2041 Self::ServerError => "server_error",
2042 Self::UnknownError => "unknown",
2043 Self::MaxOutputTokens => "max_output_tokens",
2044 Self::Unknown(s) => s.as_str(),
2045 }
2046 }
2047}
2048
2049impl fmt::Display for AssistantErrorKind {
2050 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2051 f.write_str(self.as_str())
2052 }
2053}
2054
2055impl From<&str> for AssistantErrorKind {
2056 fn from(s: &str) -> Self {
2057 match s {
2058 "authentication_failed" => Self::AuthenticationFailed,
2059 "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
2060 "billing_error" => Self::BillingError,
2061 "rate_limit" => Self::RateLimit,
2062 "overloaded" => Self::Overloaded,
2063 "invalid_request" => Self::InvalidRequest,
2064 "model_not_found" => Self::ModelNotFound,
2065 "server_error" => Self::ServerError,
2066 "unknown" => Self::UnknownError,
2067 "max_output_tokens" => Self::MaxOutputTokens,
2068 other => Self::Unknown(other.to_string()),
2069 }
2070 }
2071}
2072
2073impl Serialize for AssistantErrorKind {
2074 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2075 serializer.serialize_str(self.as_str())
2076 }
2077}
2078
2079impl<'de> Deserialize<'de> for AssistantErrorKind {
2080 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2081 let s = String::deserialize(deserializer)?;
2082 Ok(Self::from(s.as_str()))
2083 }
2084}
2085
2086#[derive(Debug, Clone, Serialize, Deserialize)]
2093pub struct CodeChangePublishedMessage {
2094 pub provider: String,
2098 pub url: String,
2100 pub repo: String,
2103 pub identifier: String,
2105 pub uuid: String,
2106 pub session_id: String,
2107}
2108
2109#[derive(Debug, Clone, Serialize, Deserialize)]
2114pub struct VcsStateChangedMessage {
2115 pub kind: VcsMutationKind,
2118 pub cwd: String,
2121 pub uuid: String,
2122 pub session_id: String,
2123}
2124
2125#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2127pub enum VcsMutationKind {
2128 Commit,
2129 Push,
2130 Merge,
2131 Rebase,
2132 Unknown(String),
2134}
2135
2136impl VcsMutationKind {
2137 pub fn as_str(&self) -> &str {
2138 match self {
2139 Self::Commit => "commit",
2140 Self::Push => "push",
2141 Self::Merge => "merge",
2142 Self::Rebase => "rebase",
2143 Self::Unknown(s) => s.as_str(),
2144 }
2145 }
2146}
2147
2148impl fmt::Display for VcsMutationKind {
2149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2150 f.write_str(self.as_str())
2151 }
2152}
2153
2154impl From<&str> for VcsMutationKind {
2155 fn from(s: &str) -> Self {
2156 match s {
2157 "commit" => Self::Commit,
2158 "push" => Self::Push,
2159 "merge" => Self::Merge,
2160 "rebase" => Self::Rebase,
2161 other => Self::Unknown(other.to_string()),
2162 }
2163 }
2164}
2165
2166impl Serialize for VcsMutationKind {
2167 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2168 serializer.serialize_str(self.as_str())
2169 }
2170}
2171
2172impl<'de> Deserialize<'de> for VcsMutationKind {
2173 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2174 let s = String::deserialize(deserializer)?;
2175 Ok(Self::from(s.as_str()))
2176 }
2177}
2178
2179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2181pub struct ToolUseMeta {
2182 pub id: String,
2183 pub display_name: String,
2184 #[serde(default, skip_serializing_if = "Option::is_none")]
2185 pub server_display_name: Option<String>,
2186 #[serde(default, skip_serializing_if = "Option::is_none")]
2187 pub icon_url: Option<String>,
2188}
2189
2190#[derive(Debug, Clone, Serialize, Deserialize)]
2192pub struct AssistantMessage {
2193 pub message: AssistantMessageContent,
2194 #[serde(alias = "sessionId")]
2195 pub session_id: String,
2196 #[serde(skip_serializing_if = "Option::is_none")]
2197 pub uuid: Option<String>,
2198 #[serde(skip_serializing_if = "Option::is_none")]
2199 pub parent_tool_use_id: Option<String>,
2200 #[serde(skip_serializing_if = "Option::is_none")]
2202 pub request_id: Option<String>,
2203 #[serde(skip_serializing_if = "Option::is_none")]
2206 pub subagent_type: Option<String>,
2207 #[serde(skip_serializing_if = "Option::is_none")]
2209 pub task_description: Option<String>,
2210 #[serde(skip_serializing_if = "Option::is_none")]
2211 pub error: Option<AssistantErrorKind>,
2212 #[serde(default, skip_serializing_if = "Option::is_none")]
2216 pub aborted: Option<bool>,
2217 #[serde(default, skip_serializing_if = "Option::is_none")]
2222 pub resumed_from_incomplete_thinking: Option<bool>,
2223 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2224 pub supersedes: Vec<String>,
2225 #[serde(skip_serializing_if = "Option::is_none")]
2226 pub timestamp: Option<String>,
2227 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2228 pub tool_use_meta: Vec<ToolUseMeta>,
2229 #[serde(default, skip_serializing_if = "Option::is_none")]
2230 pub is_meta: Option<bool>,
2231 #[serde(default, skip_serializing_if = "Option::is_none")]
2232 pub is_virtual: Option<bool>,
2233 #[serde(default, skip_serializing_if = "Option::is_none")]
2234 pub is_api_error_message: Option<bool>,
2235 #[serde(skip_serializing_if = "Option::is_none")]
2236 pub api_error_status: Option<u16>,
2237 #[serde(skip_serializing_if = "Option::is_none")]
2238 pub api_error: Option<String>,
2239 #[serde(skip_serializing_if = "Option::is_none")]
2240 pub error_details: Option<String>,
2241 #[serde(skip_serializing_if = "Option::is_none")]
2242 pub advisor_model: Option<String>,
2243 #[serde(skip_serializing_if = "Option::is_none")]
2244 pub attribution_agent: Option<String>,
2245 #[serde(skip_serializing_if = "Option::is_none")]
2246 pub attribution_skill: Option<String>,
2247 #[serde(skip_serializing_if = "Option::is_none")]
2248 pub attribution_plugin: Option<String>,
2249 #[serde(skip_serializing_if = "Option::is_none")]
2250 pub attribution_mcp_server: Option<String>,
2251 #[serde(skip_serializing_if = "Option::is_none")]
2252 pub attribution_mcp_tool: Option<String>,
2253}
2254
2255#[derive(Debug, Clone, Serialize, Deserialize)]
2257pub struct AssistantMessageContent {
2258 pub id: String,
2259 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2261 pub message_type: Option<String>,
2262 pub role: MessageRole,
2263 pub model: String,
2264 pub content: Vec<ContentBlock>,
2265 #[serde(skip_serializing_if = "Option::is_none")]
2266 pub stop_reason: Option<StopReason>,
2267 #[serde(skip_serializing_if = "Option::is_none")]
2268 pub stop_sequence: Option<String>,
2269 #[serde(skip_serializing_if = "Option::is_none")]
2270 pub usage: Option<AssistantUsage>,
2271 #[serde(skip_serializing_if = "Option::is_none")]
2273 pub stop_details: Option<Value>,
2274 #[serde(skip_serializing_if = "Option::is_none")]
2276 pub context_management: Option<Value>,
2277}
2278
2279#[derive(Debug, Clone, Serialize, Deserialize)]
2281pub struct AssistantUsage {
2282 #[serde(default)]
2284 pub input_tokens: u32,
2285
2286 #[serde(default)]
2288 pub output_tokens: u32,
2289
2290 #[serde(default)]
2292 pub cache_creation_input_tokens: u32,
2293
2294 #[serde(default)]
2296 pub cache_read_input_tokens: u32,
2297
2298 #[serde(skip_serializing_if = "Option::is_none")]
2300 pub service_tier: Option<String>,
2301
2302 #[serde(skip_serializing_if = "Option::is_none")]
2304 pub cache_creation: Option<CacheCreationDetails>,
2305
2306 #[serde(skip_serializing_if = "Option::is_none")]
2308 pub inference_geo: Option<String>,
2309}
2310
2311#[derive(Debug, Clone, Serialize, Deserialize)]
2313pub struct CacheCreationDetails {
2314 #[serde(default)]
2316 pub ephemeral_1h_input_tokens: u32,
2317
2318 #[serde(default)]
2320 pub ephemeral_5m_input_tokens: u32,
2321}
2322
2323#[cfg(test)]
2324mod tests {
2325 use crate::io::ClaudeOutput;
2326
2327 #[test]
2328 fn test_subagent_usage_rollup_accumulates_task_results() {
2329 use super::SubagentUsageRollup;
2330
2331 let mut rollup = SubagentUsageRollup::default();
2332
2333 let task_result = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"status":"completed","prompt":"Compute 6 times 7.","agentId":"ab52f22445470d454","agentType":"general-purpose","resolvedModel":"claude-sonnet-4-6","totalDurationMs":1853,"totalTokens":10201,"totalToolUseCount":3}}"#;
2334 let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2335 assert!(rollup.observe(&output));
2336 assert_eq!(rollup.subagent_tokens, 10201);
2337 assert_eq!(rollup.agent_count, 1);
2338 assert_eq!(rollup.tool_uses, 3);
2339 assert_eq!(rollup.duration_ms, 1853);
2340
2341 assert!(!rollup.observe(&output));
2343 assert_eq!(rollup.agent_count, 1);
2344 assert_eq!(rollup.subagent_tokens, 10201);
2345
2346 let second = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"status":"completed","agentId":"ffff00001111","totalDurationMs":100,"totalTokens":500,"totalToolUseCount":1}}"#;
2348 let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2349 assert!(rollup.observe(&output));
2350 assert_eq!(rollup.agent_count, 2);
2351 assert_eq!(rollup.subagent_tokens, 10701);
2352 }
2353
2354 #[test]
2355 fn test_subagent_usage_rollup_ignores_non_task_results() {
2356 use super::SubagentUsageRollup;
2357
2358 let mut rollup = SubagentUsageRollup::default();
2359
2360 let tool_search = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"matches":["TaskCreate"],"query":"select:TaskCreate","total_deferred_tools":27}}"#;
2363 let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2364 assert!(!rollup.observe(&output));
2365
2366 let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2368 let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2369 assert!(!rollup.observe(&output));
2370
2371 let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2373 let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2374 assert!(!rollup.observe(&output));
2375
2376 assert_eq!(rollup, SubagentUsageRollup::default());
2377 }
2378
2379 #[test]
2380 fn test_subagent_usage_rollup_over_captured_session() {
2381 use super::SubagentUsageRollup;
2382
2383 let mut rollup = SubagentUsageRollup::default();
2384 let fixture =
2385 include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2386 for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2387 if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2388 rollup.observe(&output);
2389 }
2390 }
2391 assert_eq!(rollup.agent_count, 1);
2392 assert_eq!(rollup.subagent_tokens, 10201);
2393 }
2394
2395 #[test]
2396 fn test_system_message_init() {
2397 let json = r#"{
2398 "type": "system",
2399 "subtype": "init",
2400 "session_id": "test-session-123",
2401 "cwd": "/home/user/project",
2402 "model": "claude-sonnet-4",
2403 "tools": ["Bash", "Read", "Write"],
2404 "mcp_servers": [],
2405 "slash_commands": ["compact", "cost", "review"],
2406 "agents": ["Bash", "Explore", "Plan"],
2407 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2408 "skills": [],
2409 "claude_code_version": "2.1.15",
2410 "apiKeySource": "none",
2411 "output_style": "default",
2412 "permissionMode": "default"
2413 }"#;
2414
2415 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2416 if let ClaudeOutput::System(sys) = output {
2417 assert!(sys.is_init());
2418 assert!(!sys.is_status());
2419 assert!(!sys.is_compact_boundary());
2420
2421 let init = sys.as_init().expect("Should parse as init");
2422 assert_eq!(init.session_id, "test-session-123");
2423 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2424 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2425 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2426 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2427 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2428 assert_eq!(init.plugins.len(), 1);
2429 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2430 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2431 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2432 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2433 assert_eq!(
2434 init.permission_mode,
2435 Some(super::InitPermissionMode::Default)
2436 );
2437 } else {
2438 panic!("Expected System message");
2439 }
2440 }
2441
2442 #[test]
2443 fn test_system_message_init_from_real_capture() {
2444 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2445 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2446 if let ClaudeOutput::System(sys) = output {
2447 let init = sys.as_init().expect("Should parse real init capture");
2448 assert_eq!(init.slash_commands.len(), 8);
2449 assert!(init.slash_commands.contains(&"compact".to_string()));
2450 assert!(init.slash_commands.contains(&"review".to_string()));
2451 assert_eq!(init.agents.len(), 5);
2452 assert!(init.agents.contains(&"Bash".to_string()));
2453 assert!(init.agents.contains(&"Explore".to_string()));
2454 assert_eq!(init.plugins.len(), 1);
2455 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2456 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2457 } else {
2458 panic!("Expected System message");
2459 }
2460 }
2461
2462 #[test]
2463 fn test_system_message_status() {
2464 let json = r#"{
2465 "type": "system",
2466 "subtype": "status",
2467 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2468 "status": "compacting",
2469 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2470 }"#;
2471
2472 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2473 if let ClaudeOutput::System(sys) = output {
2474 assert!(sys.is_status());
2475 assert!(!sys.is_init());
2476
2477 let status = sys.as_status().expect("Should parse as status");
2478 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2479 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
2480 assert_eq!(
2481 status.uuid,
2482 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2483 );
2484 } else {
2485 panic!("Expected System message");
2486 }
2487 }
2488
2489 #[test]
2490 fn test_system_message_status_null() {
2491 let json = r#"{
2492 "type": "system",
2493 "subtype": "status",
2494 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2495 "status": null,
2496 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2497 }"#;
2498
2499 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2500 if let ClaudeOutput::System(sys) = output {
2501 let status = sys.as_status().expect("Should parse as status");
2502 assert_eq!(status.status, None);
2503 } else {
2504 panic!("Expected System message");
2505 }
2506 }
2507
2508 #[test]
2509 fn test_system_message_task_started() {
2510 let json = r#"{
2511 "type": "system",
2512 "subtype": "task_started",
2513 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2514 "task_id": "b6daf3f",
2515 "task_type": "local_bash",
2516 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
2517 "description": "Wait for CI on PR #12",
2518 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
2519 }"#;
2520
2521 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2522 if let ClaudeOutput::System(sys) = output {
2523 assert!(sys.is_task_started());
2524 assert!(!sys.is_task_progress());
2525 assert!(!sys.is_task_notification());
2526
2527 let task = sys.as_task_started().expect("Should parse as task_started");
2528 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
2529 assert_eq!(task.task_id, "b6daf3f");
2530 assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
2531 assert_eq!(
2532 task.tool_use_id.as_deref(),
2533 Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
2534 );
2535 assert_eq!(task.description, "Wait for CI on PR #12");
2536 } else {
2537 panic!("Expected System message");
2538 }
2539 }
2540
2541 #[test]
2542 fn test_system_message_task_started_agent() {
2543 let json = r#"{
2544 "type": "system",
2545 "subtype": "task_started",
2546 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2547 "task_id": "a4a7e0906e5fc64cc",
2548 "task_type": "local_agent",
2549 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2550 "description": "Explore Scene/ArrayScene duplication",
2551 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2552 }"#;
2553
2554 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2555 if let ClaudeOutput::System(sys) = output {
2556 let task = sys.as_task_started().expect("Should parse as task_started");
2557 assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
2558 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
2559 } else {
2560 panic!("Expected System message");
2561 }
2562 }
2563
2564 #[test]
2565 fn test_system_message_task_progress() {
2566 let json = r#"{
2567 "type": "system",
2568 "subtype": "task_progress",
2569 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2570 "task_id": "a4a7e0906e5fc64cc",
2571 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2572 "description": "Reading src/jplephem/chebyshev.rs",
2573 "last_tool_name": "Read",
2574 "usage": {
2575 "duration_ms": 13996,
2576 "tool_uses": 9,
2577 "total_tokens": 38779
2578 },
2579 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2580 }"#;
2581
2582 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2583 if let ClaudeOutput::System(sys) = output {
2584 assert!(sys.is_task_progress());
2585 assert!(!sys.is_task_started());
2586
2587 let progress = sys
2588 .as_task_progress()
2589 .expect("Should parse as task_progress");
2590 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
2591 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
2592 assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
2593 assert_eq!(progress.usage.duration_ms, 13996);
2594 assert_eq!(progress.usage.tool_uses, 9);
2595 assert_eq!(progress.usage.total_tokens, 38779);
2596 } else {
2597 panic!("Expected System message");
2598 }
2599 }
2600
2601 #[test]
2602 fn test_system_message_task_notification_completed() {
2603 let json = r#"{
2604 "type": "system",
2605 "subtype": "task_notification",
2606 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2607 "task_id": "a0ba761e9dc9c316f",
2608 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
2609 "status": "completed",
2610 "summary": "Agent \"Write Hipparcos data source doc\" completed",
2611 "output_file": "",
2612 "usage": {
2613 "duration_ms": 172300,
2614 "tool_uses": 11,
2615 "total_tokens": 42005
2616 },
2617 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
2618 }"#;
2619
2620 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2621 if let ClaudeOutput::System(sys) = output {
2622 assert!(sys.is_task_notification());
2623
2624 let notif = sys
2625 .as_task_notification()
2626 .expect("Should parse as task_notification");
2627 assert_eq!(notif.status, super::TaskStatus::Completed);
2628 assert_eq!(
2629 notif.summary,
2630 "Agent \"Write Hipparcos data source doc\" completed"
2631 );
2632 assert_eq!(notif.output_file, Some("".to_string()));
2633 assert_eq!(
2634 notif.tool_use_id,
2635 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
2636 );
2637 let usage = notif.usage.expect("Should have usage");
2638 assert_eq!(usage.duration_ms, 172300);
2639 assert_eq!(usage.tool_uses, 11);
2640 assert_eq!(usage.total_tokens, 42005);
2641 } else {
2642 panic!("Expected System message");
2643 }
2644 }
2645
2646 #[test]
2647 fn test_system_message_task_notification_failed_no_usage() {
2648 let json = r#"{
2649 "type": "system",
2650 "subtype": "task_notification",
2651 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
2652 "task_id": "b98f6a3",
2653 "status": "failed",
2654 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
2655 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
2656 }"#;
2657
2658 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2659 if let ClaudeOutput::System(sys) = output {
2660 let notif = sys
2661 .as_task_notification()
2662 .expect("Should parse as task_notification");
2663 assert_eq!(notif.status, super::TaskStatus::Failed);
2664 assert!(notif.tool_use_id.is_none());
2665 assert!(notif.usage.is_none());
2666 assert_eq!(
2667 notif.output_file,
2668 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
2669 );
2670 } else {
2671 panic!("Expected System message");
2672 }
2673 }
2674
2675 #[test]
2681 fn test_task_messages_roundtrip_through_value() {
2682 let cases = [
2683 r#"{"type":"system","subtype":"task_started","session_id":"s1",
2684 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
2685 "description":"Sleep 3s","uuid":"u1"}"#,
2686 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
2687 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
2688 "last_tool_name":"Bash",
2689 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2690 "uuid":"u2"}"#,
2691 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
2692 "task_id":"t1","tool_use_id":"tu1","status":"completed",
2693 "summary":"done","output_file":"",
2694 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2695 "uuid":"u3"}"#,
2696 ];
2697
2698 for json in cases {
2699 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2700 let value = serde_json::to_value(&output).unwrap();
2701 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
2702
2703 let ClaudeOutput::System(sys) = reparsed else {
2704 panic!("Expected System variant after round-trip");
2705 };
2706
2707 match sys.subtype {
2708 super::SystemSubtype::TaskStarted => {
2709 assert!(
2710 sys.as_task_started().is_some(),
2711 "as_task_started failed after round-trip"
2712 );
2713 }
2714 super::SystemSubtype::TaskProgress => {
2715 assert!(
2716 sys.as_task_progress().is_some(),
2717 "as_task_progress failed after round-trip"
2718 );
2719 }
2720 super::SystemSubtype::TaskNotification => {
2721 assert!(
2722 sys.as_task_notification().is_some(),
2723 "as_task_notification failed after round-trip"
2724 );
2725 }
2726 other => panic!("unexpected subtype after round-trip: {other:?}"),
2727 }
2728 }
2729 }
2730
2731 #[test]
2732 fn test_system_message_compact_boundary() {
2733 let json = r#"{
2734 "type": "system",
2735 "subtype": "compact_boundary",
2736 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2737 "compact_metadata": {
2738 "pre_tokens": 155285,
2739 "trigger": "auto"
2740 },
2741 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
2742 }"#;
2743
2744 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2745 if let ClaudeOutput::System(sys) = output {
2746 assert!(sys.is_compact_boundary());
2747 assert!(!sys.is_init());
2748 assert!(!sys.is_status());
2749
2750 let compact = sys
2751 .as_compact_boundary()
2752 .expect("Should parse as compact_boundary");
2753 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2754 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
2755 assert_eq!(
2756 compact.compact_metadata.trigger,
2757 super::CompactionTrigger::Auto
2758 );
2759 assert!(compact.summary.is_none());
2761 assert!(compact.leaf_message_count.is_none());
2762 assert!(compact.duration_ms.is_none());
2763 } else {
2764 panic!("Expected System message");
2765 }
2766 }
2767
2768 #[test]
2769 fn test_compact_boundary_with_summary_stats() {
2770 let json = r#"{
2772 "type": "system",
2773 "subtype": "compact_boundary",
2774 "session_id": "s1",
2775 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
2776 "summary": "Summarized the earlier exploration.",
2777 "leaf_message_count": 42,
2778 "duration_ms": 1234,
2779 "uuid": "u1"
2780 }"#;
2781 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2782 let ClaudeOutput::System(sys) = output else {
2783 panic!("Expected System message");
2784 };
2785 let compact = sys.as_compact_boundary().expect("compact_boundary");
2786 assert_eq!(
2787 compact.summary.as_deref(),
2788 Some("Summarized the earlier exploration.")
2789 );
2790 assert_eq!(compact.leaf_message_count, Some(42));
2791 assert_eq!(compact.duration_ms, Some(1234));
2792
2793 let json_alt = r#"{
2796 "type": "system",
2797 "subtype": "compact_boundary",
2798 "session_id": "s2",
2799 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
2800 "content": "alt-key summary",
2801 "message_count": 7
2802 }"#;
2803 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
2804 let ClaudeOutput::System(sys) = output else {
2805 panic!("Expected System message");
2806 };
2807 let compact = sys.as_compact_boundary().expect("compact_boundary");
2808 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
2809 assert_eq!(compact.leaf_message_count, Some(7));
2810 }
2811
2812 #[test]
2813 fn test_init_message_with_new_fields() {
2814 let json = r#"{
2815 "type": "system",
2816 "subtype": "init",
2817 "session_id": "test-session",
2818 "cwd": "/home/user",
2819 "model": "claude-opus-4-7",
2820 "tools": ["Bash"],
2821 "mcp_servers": [],
2822 "permissionMode": "default",
2823 "apiKeySource": "none",
2824 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
2825 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
2826 "fast_mode_state": "off",
2827 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
2828 "claude_code_version": "2.1.117"
2829 }"#;
2830
2831 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2832 if let ClaudeOutput::System(sys) = output {
2833 let init = sys.as_init().expect("Should parse as init");
2834 assert_eq!(
2835 init.uuid.as_deref(),
2836 Some("44841a0d-182d-493a-86b5-79800d3d9665")
2837 );
2838 assert!(init.memory_paths.is_some());
2839 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
2840 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
2841 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
2842 } else {
2843 panic!("Expected System message");
2844 }
2845 }
2846
2847 #[test]
2848 fn test_assistant_message_with_new_fields() {
2849 let json = r#"{
2850 "type": "assistant",
2851 "message": {
2852 "id": "msg_1",
2853 "type": "message",
2854 "role": "assistant",
2855 "model": "claude-opus-4-7",
2856 "content": [{"type": "text", "text": "Hello"}],
2857 "stop_reason": "end_turn",
2858 "stop_details": null,
2859 "context_management": null,
2860 "usage": {
2861 "input_tokens": 100,
2862 "output_tokens": 10,
2863 "cache_creation_input_tokens": 50,
2864 "cache_read_input_tokens": 0,
2865 "service_tier": "standard",
2866 "inference_geo": "not_available"
2867 }
2868 },
2869 "session_id": "abc",
2870 "uuid": "msg-uuid-123"
2871 }"#;
2872
2873 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2874 if let ClaudeOutput::Assistant(asst) = output {
2875 assert_eq!(asst.message.stop_details, None);
2876 assert_eq!(asst.message.context_management, None);
2877 let usage = asst.message.usage.unwrap();
2878 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
2879 } else {
2880 panic!("Expected Assistant message");
2881 }
2882 }
2883
2884 #[test]
2885 fn test_user_message_with_new_fields() {
2886 let json = r#"{
2887 "type": "user",
2888 "message": {
2889 "role": "user",
2890 "content": [{"type": "text", "text": "Hello"}]
2891 },
2892 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2893 "parent_tool_use_id": "toolu_123",
2894 "uuid": "user-msg-456"
2895 }"#;
2896
2897 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2898 if let ClaudeOutput::User(user) = output {
2899 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
2900 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
2901 } else {
2902 panic!("Expected User message");
2903 }
2904 }
2905
2906 #[test]
2912 fn test_user_message_preserves_tool_use_result_and_timestamp() {
2913 let json = r#"{
2914 "type":"user",
2915 "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"}]},
2916 "parent_tool_use_id":null,
2917 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
2918 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
2919 "timestamp":"2026-05-12T23:12:04.121Z",
2920 "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"}}
2921 }"#;
2922
2923 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2924 let user = match output {
2925 ClaudeOutput::User(u) => u,
2926 other => panic!("Expected User message, got {:?}", other.message_type()),
2927 };
2928
2929 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
2930 let raw = user
2931 .tool_use_result
2932 .as_ref()
2933 .expect("tool_use_result must be captured");
2934 assert_eq!(raw["answers"]["Color"], "Blue");
2935 assert_eq!(raw["questions"][0]["header"], "Color");
2936
2937 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
2941 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
2942 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
2943 assert_eq!(
2944 reser["tool_use_result"]["questions"][0]["question"],
2945 "Which color do you prefer?"
2946 );
2947
2948 let typed: crate::AskUserQuestionInput = user
2951 .tool_use_result_as::<crate::AskUserQuestionInput>()
2952 .expect("tool_use_result present")
2953 .expect("AskUserQuestionInput parses");
2954 assert_eq!(typed.questions.len(), 1);
2955 assert_eq!(typed.questions[0].header, "Color");
2956 let answers = typed.answers.expect("answers populated");
2957 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
2958 }
2959
2960 #[test]
2963 fn test_user_message_without_tool_use_result_omits_field() {
2964 let json = r#"{
2965 "type":"user",
2966 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
2967 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
2968 }"#;
2969
2970 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2971 let user = match output {
2972 ClaudeOutput::User(u) => u,
2973 _ => panic!("Expected User message"),
2974 };
2975 assert!(user.tool_use_result.is_none());
2976 assert!(user.timestamp.is_none());
2977
2978 let reser = serde_json::to_value(&user).unwrap();
2979 assert!(reser.get("tool_use_result").is_none());
2980 assert!(reser.get("timestamp").is_none());
2981 }
2982
2983 #[test]
2987 fn test_subagent_result_exposes_token_accounting() {
2988 let json = r#"{
2989 "type":"user",
2990 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
2991 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
2992 "tool_use_result":{
2993 "status":"completed",
2994 "prompt":"Count the .rs files.",
2995 "agentId":"ac4f0276e9d4b6232",
2996 "agentType":"Explore",
2997 "content":[{"type":"text","text":"21"}],
2998 "resolvedModel":"claude-haiku-4-5-20251001",
2999 "totalDurationMs":6869,
3000 "totalTokens":7834,
3001 "totalToolUseCount":1,
3002 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3003 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3004 }
3005 }"#;
3006
3007 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3008 let user = match output {
3009 ClaudeOutput::User(u) => u,
3010 _ => panic!("Expected User message"),
3011 };
3012
3013 let result = user.subagent_result().expect("subagent result parses");
3014 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3015 assert_eq!(
3016 result.resolved_model.as_deref(),
3017 Some("claude-haiku-4-5-20251001")
3018 );
3019 assert_eq!(result.total_tokens, Some(7834));
3020 assert_eq!(result.total_duration_ms, Some(6869));
3021 assert_eq!(result.total_tool_use_count, Some(1));
3022
3023 let usage = result.usage.expect("nested usage present");
3024 assert_eq!(usage.input_tokens, 6);
3025 assert_eq!(usage.cache_read_input_tokens, 7699);
3026
3027 let stats = result.tool_stats.expect("toolStats present");
3028 assert_eq!(stats.bash_count, 1);
3029 }
3030
3031 #[test]
3035 fn test_subagent_result_absent_for_non_task_result() {
3036 let json = r#"{
3037 "type":"user",
3038 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3039 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3040 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3041 }"#;
3042
3043 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3044 let user = match output {
3045 ClaudeOutput::User(u) => u,
3046 _ => panic!("Expected User message"),
3047 };
3048
3049 let result = user.subagent_result().expect("lenient parse");
3050 assert_eq!(result.total_tokens, None);
3051 assert_eq!(result.agent_type, None);
3052 }
3053
3054 #[test]
3055 fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3056 use serde_json::Value;
3057
3058 let raw: Value = serde_json::from_str(
3059 r#"{
3060 "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3061 "fast_mode_state":"off",
3062 "fast_mode_disabled_reason":"not_first_party",
3063 "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3064 }"#,
3065 )
3066 .unwrap();
3067 crate::io::assert_fully_wrapped(&raw);
3068
3069 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3070 let ClaudeOutput::System(sys) = output else {
3071 panic!("expected System");
3072 };
3073 let init = sys.as_init().expect("parses as init");
3074 assert_eq!(
3075 init.fast_mode_disabled_reason,
3076 Some(crate::FastModeDisabledReason::NotFirstParty)
3077 );
3078 let errs = init.mcp_server_errors.unwrap();
3079 assert_eq!(errs.len(), 1);
3080 assert_eq!(errs[0].name, "broken");
3081 assert_eq!(errs[0].error_type, "invalid_config");
3082 }
3083
3084 #[test]
3085 fn test_code_change_published_fully_wrapped() {
3086 use super::{KnownSystemEvent, SystemSubtype};
3087 use serde_json::Value;
3088
3089 let raw: Value = serde_json::from_str(
3090 r#"{
3091 "type":"system","subtype":"code_change_published",
3092 "provider":"github","url":"https://github.com/owner/repo/pull/42",
3093 "repo":"owner/repo","identifier":"42",
3094 "uuid":"u1","session_id":"s1"
3095 }"#,
3096 )
3097 .unwrap();
3098 crate::io::assert_fully_wrapped(&raw);
3099
3100 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3101 let ClaudeOutput::System(sys) = output else {
3102 panic!("expected System");
3103 };
3104 assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3105 let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3106 panic!("expected CodeChangePublished event");
3107 };
3108 assert_eq!(msg.provider, "github");
3109 assert_eq!(msg.repo, "owner/repo");
3110 assert_eq!(msg.identifier, "42");
3111
3112 assert!(sys.is_code_change_published());
3113 assert!(!sys.is_vcs_state_changed());
3114 let direct = sys.as_code_change_published().expect("direct accessor");
3115 assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3116 assert!(sys.as_vcs_state_changed().is_none());
3117 }
3118
3119 #[test]
3120 fn test_vcs_state_changed_fully_wrapped() {
3121 use super::{KnownSystemEvent, VcsMutationKind};
3122 use serde_json::Value;
3123
3124 for kind in ["commit", "push", "merge", "rebase"] {
3125 let raw: Value = serde_json::from_str(&format!(
3126 r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3127 kind
3128 ))
3129 .unwrap();
3130 crate::io::assert_fully_wrapped(&raw);
3131
3132 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3133 let ClaudeOutput::System(sys) = output else {
3134 panic!("expected System");
3135 };
3136 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3137 panic!("expected VcsStateChanged event");
3138 };
3139 assert_eq!(msg.kind.as_str(), kind);
3140 assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3141 }
3142
3143 let raw: Value = serde_json::from_str(
3145 r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3146 )
3147 .unwrap();
3148 crate::io::assert_fully_wrapped(&raw);
3149 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3150 let ClaudeOutput::System(sys) = output else {
3151 panic!("expected System");
3152 };
3153 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3154 panic!("expected VcsStateChanged event");
3155 };
3156 assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3157
3158 assert!(sys.is_vcs_state_changed());
3159 let direct = sys.as_vcs_state_changed().expect("direct accessor");
3160 assert_eq!(direct.cwd, "/repo");
3161 assert!(sys.as_code_change_published().is_none());
3162 }
3163
3164 #[test]
3165 fn test_assistant_aborted_and_resume_flags_roundtrip() {
3166 let json = r#"{
3167 "type":"assistant",
3168 "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3169 "session_id":"s1",
3170 "aborted":true,
3171 "resumed_from_incomplete_thinking":true
3172 }"#;
3173 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3174 let ClaudeOutput::Assistant(msg) = &output else {
3175 panic!("expected Assistant");
3176 };
3177 assert_eq!(msg.aborted, Some(true));
3178 assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3179 let reserialized = serde_json::to_string(&output).unwrap();
3180 assert!(reserialized.contains("\"aborted\":true"));
3181 assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3182
3183 let json = r#"{
3185 "type":"assistant",
3186 "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3187 "session_id":"s2"
3188 }"#;
3189 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3190 let reserialized = serde_json::to_string(&output).unwrap();
3191 assert!(!reserialized.contains("aborted"));
3192 assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3193 }
3194
3195 #[test]
3196 fn test_user_tool_result_meta_roundtrip() {
3197 let json = r#"{
3198 "type":"user",
3199 "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3200 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3201 "tool_result_meta":[
3202 {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3203 {"id":"toolu_2","non_execution_kind":"permission-rule"}
3204 ]
3205 }"#;
3206 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3207 let ClaudeOutput::User(user) = &output else {
3208 panic!("expected User");
3209 };
3210 let meta = user.tool_result_meta.as_ref().unwrap();
3211 assert_eq!(meta.len(), 2);
3212 assert_eq!(meta[0].non_execution_kind, "user-rejected");
3213 assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3214 assert_eq!(meta[1].user_feedback, None);
3215
3216 let reserialized = serde_json::to_string(&output).unwrap();
3217 assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3218 assert!(!reserialized.contains("\"user_feedback\":null"));
3219 }
3220
3221 #[test]
3225 fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3226 use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3227 let with_scope = serde_json::json!({
3228 "trigger": "refusal",
3229 "direction": "retry",
3230 "scope": "local",
3231 "original_model": "claude-fable-5",
3232 "fallback_model": "claude-opus-5",
3233 "request_id": null,
3234 "content": "Refused; retried on fallback model.",
3235 "uuid": "u1",
3236 "session_id": "s1"
3237 });
3238 let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3239 assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3240 assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3241
3242 let mut without = with_scope.clone();
3245 without.as_object_mut().unwrap().remove("scope");
3246 let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3247 assert_eq!(msg.scope, None);
3248 assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3249
3250 assert_eq!(
3252 RefusalFallbackScope::from("workspace").as_str(),
3253 "workspace"
3254 );
3255 }
3256}