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 FeedbackDraftQueued,
47 CloudSessionDelta,
48 DevIntent,
49 Unknown(String),
51}
52
53impl SystemSubtype {
54 pub fn as_str(&self) -> &str {
55 match self {
56 Self::Init => "init",
57 Self::Status => "status",
58 Self::CompactBoundary => "compact_boundary",
59 Self::ThinkingTokens => "thinking_tokens",
60 Self::TaskStarted => "task_started",
61 Self::TaskProgress => "task_progress",
62 Self::TaskUpdated => "task_updated",
63 Self::TaskNotification => "task_notification",
64 Self::ApiRetry => "api_retry",
65 Self::ControlRequestProgress => "control_request_progress",
66 Self::ModelRefusalFallback => "model_refusal_fallback",
67 Self::ModelRefusalNoFallback => "model_refusal_no_fallback",
68 Self::LocalCommandOutput => "local_command_output",
69 Self::HookStarted => "hook_started",
70 Self::HookProgress => "hook_progress",
71 Self::HookResponse => "hook_response",
72 Self::PluginInstall => "plugin_install",
73 Self::BackgroundTasksChanged => "background_tasks_changed",
74 Self::SessionStateChanged => "session_state_changed",
75 Self::WorkerShuttingDown => "worker_shutting_down",
76 Self::CommandsChanged => "commands_changed",
77 Self::Notification => "notification",
78 Self::FilesPersisted => "files_persisted",
79 Self::MemoryRecall => "memory_recall",
80 Self::ElicitationComplete => "elicitation_complete",
81 Self::PermissionDenied => "permission_denied",
82 Self::MirrorError => "mirror_error",
83 Self::Informational => "informational",
84 Self::CodeChangePublished => "code_change_published",
85 Self::VcsStateChanged => "vcs_state_changed",
86 Self::FeedbackDraftQueued => "feedback_draft_queued",
87 Self::CloudSessionDelta => "cloud_session_delta",
88 Self::DevIntent => "dev_intent",
89 Self::Unknown(s) => s.as_str(),
90 }
91 }
92}
93
94impl fmt::Display for SystemSubtype {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100impl From<&str> for SystemSubtype {
101 fn from(s: &str) -> Self {
102 match s {
103 "init" => Self::Init,
104 "status" => Self::Status,
105 "compact_boundary" => Self::CompactBoundary,
106 "thinking_tokens" => Self::ThinkingTokens,
107 "task_started" => Self::TaskStarted,
108 "task_progress" => Self::TaskProgress,
109 "task_updated" => Self::TaskUpdated,
110 "task_notification" => Self::TaskNotification,
111 "api_retry" => Self::ApiRetry,
112 "control_request_progress" => Self::ControlRequestProgress,
113 "model_refusal_fallback" => Self::ModelRefusalFallback,
114 "model_refusal_no_fallback" => Self::ModelRefusalNoFallback,
115 "local_command_output" => Self::LocalCommandOutput,
116 "hook_started" => Self::HookStarted,
117 "hook_progress" => Self::HookProgress,
118 "hook_response" => Self::HookResponse,
119 "plugin_install" => Self::PluginInstall,
120 "background_tasks_changed" => Self::BackgroundTasksChanged,
121 "session_state_changed" => Self::SessionStateChanged,
122 "worker_shutting_down" => Self::WorkerShuttingDown,
123 "commands_changed" => Self::CommandsChanged,
124 "notification" => Self::Notification,
125 "files_persisted" => Self::FilesPersisted,
126 "memory_recall" => Self::MemoryRecall,
127 "elicitation_complete" => Self::ElicitationComplete,
128 "permission_denied" => Self::PermissionDenied,
129 "mirror_error" => Self::MirrorError,
130 "informational" => Self::Informational,
131 "code_change_published" => Self::CodeChangePublished,
132 "vcs_state_changed" => Self::VcsStateChanged,
133 "feedback_draft_queued" => Self::FeedbackDraftQueued,
134 "cloud_session_delta" => Self::CloudSessionDelta,
135 "dev_intent" => Self::DevIntent,
136 other => Self::Unknown(other.to_string()),
137 }
138 }
139}
140
141impl Serialize for SystemSubtype {
142 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
143 serializer.serialize_str(self.as_str())
144 }
145}
146
147impl<'de> Deserialize<'de> for SystemSubtype {
148 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
149 let s = String::deserialize(deserializer)?;
150 Ok(Self::from(s.as_str()))
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash)]
159pub enum MessageRole {
160 User,
161 Assistant,
162 Unknown(String),
164}
165
166impl MessageRole {
167 pub fn as_str(&self) -> &str {
168 match self {
169 Self::User => "user",
170 Self::Assistant => "assistant",
171 Self::Unknown(s) => s.as_str(),
172 }
173 }
174}
175
176impl fmt::Display for MessageRole {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str(self.as_str())
179 }
180}
181
182impl From<&str> for MessageRole {
183 fn from(s: &str) -> Self {
184 match s {
185 "user" => Self::User,
186 "assistant" => Self::Assistant,
187 other => Self::Unknown(other.to_string()),
188 }
189 }
190}
191
192impl Serialize for MessageRole {
193 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
194 serializer.serialize_str(self.as_str())
195 }
196}
197
198impl<'de> Deserialize<'de> for MessageRole {
199 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
200 let s = String::deserialize(deserializer)?;
201 Ok(Self::from(s.as_str()))
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Hash)]
207pub enum CompactionTrigger {
208 Auto,
210 Manual,
212 Unknown(String),
214}
215
216impl CompactionTrigger {
217 pub fn as_str(&self) -> &str {
218 match self {
219 Self::Auto => "auto",
220 Self::Manual => "manual",
221 Self::Unknown(s) => s.as_str(),
222 }
223 }
224}
225
226impl fmt::Display for CompactionTrigger {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 f.write_str(self.as_str())
229 }
230}
231
232impl From<&str> for CompactionTrigger {
233 fn from(s: &str) -> Self {
234 match s {
235 "auto" => Self::Auto,
236 "manual" => Self::Manual,
237 other => Self::Unknown(other.to_string()),
238 }
239 }
240}
241
242impl Serialize for CompactionTrigger {
243 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
244 serializer.serialize_str(self.as_str())
245 }
246}
247
248impl<'de> Deserialize<'de> for CompactionTrigger {
249 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
250 let s = String::deserialize(deserializer)?;
251 Ok(Self::from(s.as_str()))
252 }
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub enum StopReason {
258 EndTurn,
260 MaxTokens,
262 ToolUse,
264 Unknown(String),
266}
267
268impl StopReason {
269 pub fn as_str(&self) -> &str {
270 match self {
271 Self::EndTurn => "end_turn",
272 Self::MaxTokens => "max_tokens",
273 Self::ToolUse => "tool_use",
274 Self::Unknown(s) => s.as_str(),
275 }
276 }
277}
278
279impl fmt::Display for StopReason {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 f.write_str(self.as_str())
282 }
283}
284
285impl From<&str> for StopReason {
286 fn from(s: &str) -> Self {
287 match s {
288 "end_turn" => Self::EndTurn,
289 "max_tokens" => Self::MaxTokens,
290 "tool_use" => Self::ToolUse,
291 other => Self::Unknown(other.to_string()),
292 }
293 }
294}
295
296impl Serialize for StopReason {
297 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
298 serializer.serialize_str(self.as_str())
299 }
300}
301
302impl<'de> Deserialize<'de> for StopReason {
303 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
304 let s = String::deserialize(deserializer)?;
305 Ok(Self::from(s.as_str()))
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Hash)]
311pub enum ApiKeySource {
312 None,
314 User,
315 Project,
316 Org,
317 Temporary,
318 Oauth,
319 Unknown(String),
321}
322
323impl ApiKeySource {
324 pub fn as_str(&self) -> &str {
325 match self {
326 Self::None => "none",
327 Self::User => "user",
328 Self::Project => "project",
329 Self::Org => "org",
330 Self::Temporary => "temporary",
331 Self::Oauth => "oauth",
332 Self::Unknown(s) => s.as_str(),
333 }
334 }
335}
336
337impl fmt::Display for ApiKeySource {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 f.write_str(self.as_str())
340 }
341}
342
343impl From<&str> for ApiKeySource {
344 fn from(s: &str) -> Self {
345 match s {
346 "none" => Self::None,
347 "user" => Self::User,
348 "project" => Self::Project,
349 "org" => Self::Org,
350 "temporary" => Self::Temporary,
351 "oauth" => Self::Oauth,
352 other => Self::Unknown(other.to_string()),
353 }
354 }
355}
356
357impl Serialize for ApiKeySource {
358 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
359 serializer.serialize_str(self.as_str())
360 }
361}
362
363impl<'de> Deserialize<'de> for ApiKeySource {
364 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
365 let s = String::deserialize(deserializer)?;
366 Ok(Self::from(s.as_str()))
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Hash)]
372pub enum OutputStyle {
373 Default,
375 Unknown(String),
377}
378
379impl OutputStyle {
380 pub fn as_str(&self) -> &str {
381 match self {
382 Self::Default => "default",
383 Self::Unknown(s) => s.as_str(),
384 }
385 }
386}
387
388impl fmt::Display for OutputStyle {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.write_str(self.as_str())
391 }
392}
393
394impl From<&str> for OutputStyle {
395 fn from(s: &str) -> Self {
396 match s {
397 "default" => Self::Default,
398 other => Self::Unknown(other.to_string()),
399 }
400 }
401}
402
403impl Serialize for OutputStyle {
404 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
405 serializer.serialize_str(self.as_str())
406 }
407}
408
409impl<'de> Deserialize<'de> for OutputStyle {
410 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
411 let s = String::deserialize(deserializer)?;
412 Ok(Self::from(s.as_str()))
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Hash)]
418pub enum InitPermissionMode {
419 Default,
421 AcceptEdits,
422 BypassPermissions,
423 Plan,
424 DontAsk,
425 Auto,
426 Unknown(String),
428}
429
430impl InitPermissionMode {
431 pub fn as_str(&self) -> &str {
432 match self {
433 Self::Default => "default",
434 Self::AcceptEdits => "acceptEdits",
435 Self::BypassPermissions => "bypassPermissions",
436 Self::Plan => "plan",
437 Self::DontAsk => "dontAsk",
438 Self::Auto => "auto",
439 Self::Unknown(s) => s.as_str(),
440 }
441 }
442}
443
444impl fmt::Display for InitPermissionMode {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 f.write_str(self.as_str())
447 }
448}
449
450impl From<&str> for InitPermissionMode {
451 fn from(s: &str) -> Self {
452 match s {
453 "default" => Self::Default,
454 "acceptEdits" => Self::AcceptEdits,
455 "bypassPermissions" => Self::BypassPermissions,
456 "plan" => Self::Plan,
457 "dontAsk" => Self::DontAsk,
458 "auto" => Self::Auto,
459 other => Self::Unknown(other.to_string()),
460 }
461 }
462}
463
464impl Serialize for InitPermissionMode {
465 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
466 serializer.serialize_str(self.as_str())
467 }
468}
469
470impl<'de> Deserialize<'de> for InitPermissionMode {
471 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
472 let s = String::deserialize(deserializer)?;
473 Ok(Self::from(s.as_str()))
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Hash)]
479pub enum StatusMessageStatus {
480 Compacting,
482 Requesting,
484 Unknown(String),
486}
487
488impl StatusMessageStatus {
489 pub fn as_str(&self) -> &str {
490 match self {
491 Self::Compacting => "compacting",
492 Self::Requesting => "requesting",
493 Self::Unknown(s) => s.as_str(),
494 }
495 }
496}
497
498impl fmt::Display for StatusMessageStatus {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 f.write_str(self.as_str())
501 }
502}
503
504impl From<&str> for StatusMessageStatus {
505 fn from(s: &str) -> Self {
506 match s {
507 "compacting" => Self::Compacting,
508 "requesting" => Self::Requesting,
509 other => Self::Unknown(other.to_string()),
510 }
511 }
512}
513
514impl Serialize for StatusMessageStatus {
515 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
516 serializer.serialize_str(self.as_str())
517 }
518}
519
520impl<'de> Deserialize<'de> for StatusMessageStatus {
521 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
522 let s = String::deserialize(deserializer)?;
523 Ok(Self::from(s.as_str()))
524 }
525}
526
527pub(crate) fn serialize_optional_uuid<S>(
529 uuid: &Option<Uuid>,
530 serializer: S,
531) -> Result<S::Ok, S::Error>
532where
533 S: Serializer,
534{
535 match uuid {
536 Some(id) => serializer.serialize_str(&id.to_string()),
537 None => serializer.serialize_none(),
538 }
539}
540
541pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
543where
544 D: Deserializer<'de>,
545{
546 let opt_str: Option<String> = Option::deserialize(deserializer)?;
547 match opt_str {
548 Some(s) => Uuid::parse_str(&s)
549 .map(Some)
550 .map_err(serde::de::Error::custom),
551 None => Ok(None),
552 }
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
558pub struct MessageOrigin {
559 pub kind: String,
560 #[serde(flatten)]
561 pub extra: serde_json::Map<String, Value>,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
566pub struct SummarizeMetadata {
567 pub messages_summarized: u64,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
569 pub user_context: Option<String>,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
571 pub direction: Option<String>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
576pub struct McpMeta {
577 #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
578 pub meta: Option<Value>,
579 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub structured_content: Option<Value>,
581 #[serde(default, skip_serializing_if = "Vec::is_empty")]
586 pub resource_links: Vec<ResourceLink>,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
593pub struct ResourceLink {
594 pub uri: String,
595 pub name: String,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
597 pub title: Option<String>,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub description: Option<String>,
600 #[serde(default, skip_serializing_if = "Option::is_none", rename = "mimeType")]
601 pub mime_type: Option<String>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub size: Option<u64>,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub annotations: Option<Value>,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
610pub struct ToolResultMeta {
611 pub id: String,
613 pub non_execution_kind: String,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub user_feedback: Option<String>,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct UserMessage {
627 pub message: MessageContent,
628 #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
629 #[serde(
630 serialize_with = "serialize_optional_uuid",
631 deserialize_with = "deserialize_optional_uuid"
632 )]
633 pub session_id: Option<Uuid>,
634 #[serde(skip_serializing_if = "Option::is_none")]
636 pub parent_tool_use_id: Option<String>,
637 #[serde(skip_serializing_if = "Option::is_none")]
639 pub uuid: Option<String>,
640 #[serde(skip_serializing_if = "Option::is_none")]
642 pub timestamp: Option<String>,
643 #[serde(skip_serializing_if = "Option::is_none")]
650 pub tool_use_result: Option<serde_json::Value>,
651 #[serde(skip_serializing_if = "Option::is_none")]
654 pub subagent_type: Option<String>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub task_description: Option<String>,
658 #[serde(skip_serializing_if = "Option::is_none")]
659 pub origin: Option<MessageOrigin>,
660 #[serde(skip_serializing_if = "Option::is_none")]
661 pub priority: Option<String>,
662 #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
663 pub is_synthetic: Option<bool>,
664 #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
665 pub should_query: Option<bool>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub is_meta: Option<bool>,
668 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub is_visible_in_transcript_only: Option<bool>,
670 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub is_virtual: Option<bool>,
672 #[serde(default, skip_serializing_if = "Option::is_none")]
673 pub is_compact_summary: Option<bool>,
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub summarize_metadata: Option<SummarizeMetadata>,
676 #[serde(skip_serializing_if = "Option::is_none")]
677 pub mcp_meta: Option<McpMeta>,
678 #[serde(skip_serializing_if = "Option::is_none")]
681 pub tool_result_meta: Option<Vec<ToolResultMeta>>,
682 #[serde(skip_serializing_if = "Option::is_none")]
683 pub source_tool_use_id: Option<String>,
684 #[serde(skip_serializing_if = "Option::is_none")]
685 pub source_tool_assistant_uuid: Option<String>,
686 #[serde(skip_serializing_if = "Option::is_none")]
687 pub image_paste_ids: Option<Vec<u64>>,
688 #[serde(skip_serializing_if = "Option::is_none")]
689 pub client_platform: Option<String>,
690 #[serde(skip_serializing_if = "Option::is_none")]
691 pub inbound_origin: Option<String>,
692 #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
693 pub is_replay: Option<bool>,
694 #[serde(skip_serializing_if = "Option::is_none")]
695 pub file_attachments: Option<Vec<Value>>,
696 #[serde(default, skip_serializing_if = "Option::is_none")]
698 pub seeded_summon: Option<bool>,
699 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub client_composed: Option<bool>,
703 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub historical: Option<bool>,
708}
709
710impl UserMessage {
711 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
719 &self,
720 ) -> Option<Result<T, serde_json::Error>> {
721 self.tool_use_result
722 .as_ref()
723 .map(|v| serde_json::from_value(v.clone()))
724 }
725
726 pub fn subagent_result(&self) -> Option<SubagentResult> {
737 self.tool_use_result
738 .as_ref()
739 .and_then(|v| serde_json::from_value(v.clone()).ok())
740 }
741}
742
743#[derive(Debug, Clone, Serialize, Deserialize)]
754pub struct SubagentResult {
755 #[serde(skip_serializing_if = "Option::is_none")]
757 pub status: Option<String>,
758 #[serde(skip_serializing_if = "Option::is_none")]
760 pub prompt: Option<String>,
761 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
763 pub agent_id: Option<String>,
764 #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
766 pub agent_type: Option<String>,
767 #[serde(
769 default,
770 deserialize_with = "deserialize_content_blocks",
771 skip_serializing_if = "Vec::is_empty"
772 )]
773 pub content: Vec<ContentBlock>,
774 #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
776 pub resolved_model: Option<String>,
777 #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
779 pub total_duration_ms: Option<u64>,
780 #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
782 pub total_tokens: Option<u64>,
783 #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
785 pub total_tool_use_count: Option<u64>,
786 #[serde(skip_serializing_if = "Option::is_none")]
788 pub usage: Option<super::result::UsageInfo>,
789 #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
791 pub tool_stats: Option<SubagentToolStats>,
792}
793
794#[derive(Debug, Clone, Default, Serialize, Deserialize)]
799#[serde(rename_all = "camelCase")]
800pub struct SubagentToolStats {
801 #[serde(default)]
802 pub read_count: u64,
803 #[serde(default)]
804 pub search_count: u64,
805 #[serde(default)]
806 pub bash_count: u64,
807 #[serde(default)]
808 pub edit_file_count: u64,
809 #[serde(default)]
810 pub lines_added: u64,
811 #[serde(default)]
812 pub lines_removed: u64,
813 #[serde(default)]
814 pub other_tool_count: u64,
815 #[serde(flatten)]
816 pub extra: serde_json::Map<String, Value>,
817}
818
819#[derive(Debug, Clone, Default, PartialEq, Eq)]
846pub struct SubagentUsageRollup {
847 pub subagent_tokens: u64,
850 pub agent_count: u32,
852 pub tool_uses: u64,
854 pub duration_ms: u64,
856 seen_agent_ids: std::collections::BTreeSet<String>,
857}
858
859impl SubagentUsageRollup {
860 pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
866 match output {
867 ClaudeOutput::User(user) => self.observe_user(user),
868 _ => false,
869 }
870 }
871
872 pub fn observe_user(&mut self, user: &UserMessage) -> bool {
879 let Some(result) = user.subagent_result() else {
880 return false;
881 };
882 if result.total_tokens.is_none() && result.agent_id.is_none() {
883 return false;
884 }
885 if let Some(agent_id) = &result.agent_id {
886 if !self.seen_agent_ids.insert(agent_id.clone()) {
887 return false;
888 }
889 }
890 self.agent_count += 1;
891 self.subagent_tokens += result.total_tokens.unwrap_or(0);
892 self.tool_uses += result.total_tool_use_count.unwrap_or(0);
893 self.duration_ms += result.total_duration_ms.unwrap_or(0);
894 true
895 }
896}
897
898#[derive(Debug, Clone, Serialize, Deserialize)]
900pub struct MessageContent {
901 pub role: MessageRole,
902 #[serde(deserialize_with = "deserialize_content_blocks")]
903 pub content: Vec<ContentBlock>,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct SystemMessage {
909 pub subtype: SystemSubtype,
910 #[serde(flatten)]
911 pub data: Value, }
913
914impl SystemMessage {
915 pub fn is_init(&self) -> bool {
917 self.subtype == SystemSubtype::Init
918 }
919
920 pub fn is_status(&self) -> bool {
922 self.subtype == SystemSubtype::Status
923 }
924
925 pub fn is_compact_boundary(&self) -> bool {
927 self.subtype == SystemSubtype::CompactBoundary
928 }
929
930 pub fn as_init(&self) -> Option<InitMessage> {
932 if self.subtype != SystemSubtype::Init {
933 return None;
934 }
935 serde_json::from_value(self.data.clone()).ok()
936 }
937
938 pub fn as_status(&self) -> Option<StatusMessage> {
940 if self.subtype != SystemSubtype::Status {
941 return None;
942 }
943 serde_json::from_value(self.data.clone()).ok()
944 }
945
946 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
948 if self.subtype != SystemSubtype::CompactBoundary {
949 return None;
950 }
951 serde_json::from_value(self.data.clone()).ok()
952 }
953
954 pub fn is_task_started(&self) -> bool {
956 self.subtype == SystemSubtype::TaskStarted
957 }
958
959 pub fn is_task_progress(&self) -> bool {
961 self.subtype == SystemSubtype::TaskProgress
962 }
963
964 pub fn is_task_notification(&self) -> bool {
966 self.subtype == SystemSubtype::TaskNotification
967 }
968
969 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
971 if self.subtype != SystemSubtype::TaskStarted {
972 return None;
973 }
974 serde_json::from_value(self.data.clone()).ok()
975 }
976
977 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
979 if self.subtype != SystemSubtype::TaskProgress {
980 return None;
981 }
982 serde_json::from_value(self.data.clone()).ok()
983 }
984
985 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
987 if self.subtype != SystemSubtype::TaskNotification {
988 return None;
989 }
990 serde_json::from_value(self.data.clone()).ok()
991 }
992
993 pub fn is_task_updated(&self) -> bool {
995 self.subtype == SystemSubtype::TaskUpdated
996 }
997
998 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
1000 if self.subtype != SystemSubtype::TaskUpdated {
1001 return None;
1002 }
1003 serde_json::from_value(self.data.clone()).ok()
1004 }
1005
1006 pub fn is_thinking_tokens(&self) -> bool {
1008 self.subtype == SystemSubtype::ThinkingTokens
1009 }
1010
1011 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
1013 if self.subtype != SystemSubtype::ThinkingTokens {
1014 return None;
1015 }
1016 serde_json::from_value(self.data.clone()).ok()
1017 }
1018
1019 pub fn is_code_change_published(&self) -> bool {
1021 self.subtype == SystemSubtype::CodeChangePublished
1022 }
1023
1024 pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
1026 if self.subtype != SystemSubtype::CodeChangePublished {
1027 return None;
1028 }
1029 serde_json::from_value(self.data.clone()).ok()
1030 }
1031
1032 pub fn is_vcs_state_changed(&self) -> bool {
1034 self.subtype == SystemSubtype::VcsStateChanged
1035 }
1036
1037 pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
1039 if self.subtype != SystemSubtype::VcsStateChanged {
1040 return None;
1041 }
1042 serde_json::from_value(self.data.clone()).ok()
1043 }
1044
1045 pub fn is_feedback_draft_queued(&self) -> bool {
1047 self.subtype == SystemSubtype::FeedbackDraftQueued
1048 }
1049
1050 pub fn as_feedback_draft_queued(&self) -> Option<FeedbackDraftQueuedMessage> {
1052 if self.subtype != SystemSubtype::FeedbackDraftQueued {
1053 return None;
1054 }
1055 serde_json::from_value(self.data.clone()).ok()
1056 }
1057
1058 pub fn is_cloud_session_delta(&self) -> bool {
1060 self.subtype == SystemSubtype::CloudSessionDelta
1061 }
1062
1063 pub fn as_cloud_session_delta(&self) -> Option<CloudSessionDeltaMessage> {
1065 if self.subtype != SystemSubtype::CloudSessionDelta {
1066 return None;
1067 }
1068 serde_json::from_value(self.data.clone()).ok()
1069 }
1070
1071 pub fn is_dev_intent(&self) -> bool {
1073 self.subtype == SystemSubtype::DevIntent
1074 }
1075
1076 pub fn as_dev_intent(&self) -> Option<DevIntentMessage> {
1078 if self.subtype != SystemSubtype::DevIntent {
1079 return None;
1080 }
1081 serde_json::from_value(self.data.clone()).ok()
1082 }
1083
1084 pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
1086 macro_rules! parse {
1087 ($variant:ident, $ty:ty) => {
1088 serde_json::from_value::<$ty>(self.data.clone())
1089 .ok()
1090 .map(KnownSystemEvent::$variant)
1091 };
1092 }
1093
1094 match self.subtype {
1095 SystemSubtype::Init => parse!(Init, InitMessage),
1096 SystemSubtype::Status => parse!(Status, StatusMessage),
1097 SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
1098 SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
1099 SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
1100 SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
1101 SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
1102 SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
1103 SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
1104 SystemSubtype::ControlRequestProgress => {
1105 parse!(ControlRequestProgress, ControlRequestProgressMessage)
1106 }
1107 SystemSubtype::ModelRefusalFallback => {
1108 parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
1109 }
1110 SystemSubtype::ModelRefusalNoFallback => {
1111 parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
1112 }
1113 SystemSubtype::LocalCommandOutput => {
1114 parse!(LocalCommandOutput, LocalCommandOutputMessage)
1115 }
1116 SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
1117 SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
1118 SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
1119 SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
1120 SystemSubtype::BackgroundTasksChanged => {
1121 parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
1122 }
1123 SystemSubtype::SessionStateChanged => {
1124 parse!(SessionStateChanged, SessionStateChangedMessage)
1125 }
1126 SystemSubtype::WorkerShuttingDown => {
1127 parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
1128 }
1129 SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
1130 SystemSubtype::Notification => parse!(Notification, NotificationMessage),
1131 SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
1132 SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
1133 SystemSubtype::ElicitationComplete => {
1134 parse!(ElicitationComplete, ElicitationCompleteMessage)
1135 }
1136 SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1137 SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1138 SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1139 SystemSubtype::CodeChangePublished => {
1140 parse!(CodeChangePublished, CodeChangePublishedMessage)
1141 }
1142 SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
1143 SystemSubtype::FeedbackDraftQueued => {
1144 parse!(FeedbackDraftQueued, FeedbackDraftQueuedMessage)
1145 }
1146 SystemSubtype::CloudSessionDelta => {
1147 parse!(CloudSessionDelta, CloudSessionDeltaMessage)
1148 }
1149 SystemSubtype::DevIntent => parse!(DevIntent, DevIntentMessage),
1150 SystemSubtype::Unknown(_) => None,
1151 }
1152 }
1153
1154 pub fn typed_value(&self) -> Option<Value> {
1163 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1164 parsed.and_then(|v| serde_json::to_value(v).ok())
1165 }
1166 match self.subtype {
1167 SystemSubtype::Init => reserialize(self.as_init()),
1168 SystemSubtype::Status => reserialize(self.as_status()),
1169 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1170 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1171 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1172 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1173 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1174 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1175 SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1176 SystemSubtype::ControlRequestProgress => {
1177 reserialize(parse_system::<ControlRequestProgressMessage>(self))
1178 }
1179 SystemSubtype::ModelRefusalFallback => {
1180 reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1181 }
1182 SystemSubtype::ModelRefusalNoFallback => {
1183 reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1184 }
1185 SystemSubtype::LocalCommandOutput => {
1186 reserialize(parse_system::<LocalCommandOutputMessage>(self))
1187 }
1188 SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1189 SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1190 SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1191 SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1192 SystemSubtype::BackgroundTasksChanged => {
1193 reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1194 }
1195 SystemSubtype::SessionStateChanged => {
1196 reserialize(parse_system::<SessionStateChangedMessage>(self))
1197 }
1198 SystemSubtype::WorkerShuttingDown => {
1199 reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1200 }
1201 SystemSubtype::CommandsChanged => {
1202 reserialize(parse_system::<CommandsChangedMessage>(self))
1203 }
1204 SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1205 SystemSubtype::FilesPersisted => {
1206 reserialize(parse_system::<FilesPersistedMessage>(self))
1207 }
1208 SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1209 SystemSubtype::ElicitationComplete => {
1210 reserialize(parse_system::<ElicitationCompleteMessage>(self))
1211 }
1212 SystemSubtype::PermissionDenied => {
1213 reserialize(parse_system::<PermissionDeniedMessage>(self))
1214 }
1215 SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1216 SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1217 SystemSubtype::CodeChangePublished => {
1218 reserialize(parse_system::<CodeChangePublishedMessage>(self))
1219 }
1220 SystemSubtype::VcsStateChanged => {
1221 reserialize(parse_system::<VcsStateChangedMessage>(self))
1222 }
1223 SystemSubtype::FeedbackDraftQueued => {
1224 reserialize(parse_system::<FeedbackDraftQueuedMessage>(self))
1225 }
1226 SystemSubtype::CloudSessionDelta => {
1227 reserialize(parse_system::<CloudSessionDeltaMessage>(self))
1228 }
1229 SystemSubtype::DevIntent => reserialize(parse_system::<DevIntentMessage>(self)),
1230 SystemSubtype::Unknown(_) => None,
1231 }
1232 }
1233}
1234
1235fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1236 serde_json::from_value(message.data.clone()).ok()
1237}
1238
1239#[allow(clippy::large_enum_variant)]
1245#[derive(Debug, Clone, Serialize, Deserialize)]
1246pub enum KnownSystemEvent {
1247 Init(InitMessage),
1248 Status(StatusMessage),
1249 CompactBoundary(CompactBoundaryMessage),
1250 ThinkingTokens(ThinkingTokensMessage),
1251 TaskStarted(TaskStartedMessage),
1252 TaskProgress(TaskProgressMessage),
1253 TaskUpdated(TaskUpdatedMessage),
1254 TaskNotification(TaskNotificationMessage),
1255 ApiRetry(ApiRetryMessage),
1256 ControlRequestProgress(ControlRequestProgressMessage),
1257 ModelRefusalFallback(ModelRefusalFallbackMessage),
1258 ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1259 LocalCommandOutput(LocalCommandOutputMessage),
1260 HookStarted(HookStartedMessage),
1261 HookProgress(HookProgressMessage),
1262 HookResponse(HookResponseMessage),
1263 PluginInstall(PluginInstallMessage),
1264 BackgroundTasksChanged(BackgroundTasksChangedMessage),
1265 SessionStateChanged(SessionStateChangedMessage),
1266 WorkerShuttingDown(WorkerShuttingDownMessage),
1267 CommandsChanged(CommandsChangedMessage),
1268 Notification(NotificationMessage),
1269 FilesPersisted(FilesPersistedMessage),
1270 MemoryRecall(MemoryRecallMessage),
1271 ElicitationComplete(ElicitationCompleteMessage),
1272 PermissionDenied(PermissionDeniedMessage),
1273 MirrorError(MirrorErrorMessage),
1274 Informational(InformationalMessage),
1275 CodeChangePublished(CodeChangePublishedMessage),
1276 VcsStateChanged(VcsStateChangedMessage),
1277 FeedbackDraftQueued(FeedbackDraftQueuedMessage),
1278 CloudSessionDelta(CloudSessionDeltaMessage),
1279 DevIntent(DevIntentMessage),
1280}
1281
1282#[derive(Debug, Clone, Serialize, Deserialize)]
1283pub struct ApiRetryMessage {
1284 pub attempt: u64,
1285 pub max_retries: u64,
1286 pub retry_delay_ms: u64,
1287 pub error_status: Option<u16>,
1288 pub error: String,
1289 #[serde(default, skip_serializing_if = "Option::is_none")]
1294 pub no_response: Option<ApiRetryNoResponse>,
1295 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub uuid: Option<String>,
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1298 pub session_id: Option<String>,
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1304pub struct ApiRetryNoResponse {
1305 pub waited_ms: u64,
1307 pub retry_wait_ms: u64,
1309}
1310
1311#[derive(Debug, Clone, Serialize, Deserialize)]
1312pub struct ControlRequestProgressMessage {
1313 pub request_id: String,
1314 pub status: String,
1315 #[serde(default, skip_serializing_if = "Option::is_none")]
1316 pub attempt: Option<u64>,
1317 #[serde(default, skip_serializing_if = "Option::is_none")]
1318 pub max_retries: Option<u64>,
1319 #[serde(default, skip_serializing_if = "Option::is_none")]
1320 pub retry_delay_ms: Option<u64>,
1321 #[serde(default, skip_serializing_if = "Option::is_none")]
1322 pub error_status: Option<u16>,
1323 #[serde(default, skip_serializing_if = "Option::is_none")]
1324 pub error: Option<String>,
1325 #[serde(default, skip_serializing_if = "Option::is_none")]
1326 pub uuid: Option<String>,
1327 #[serde(default, skip_serializing_if = "Option::is_none")]
1328 pub session_id: Option<String>,
1329}
1330
1331#[derive(Debug, Clone, Serialize, Deserialize)]
1332pub struct ModelRefusalFallbackMessage {
1333 pub trigger: String,
1334 pub direction: String,
1335 #[serde(default, skip_serializing_if = "Option::is_none")]
1341 pub scope: Option<RefusalFallbackScope>,
1342 pub original_model: String,
1343 pub fallback_model: String,
1344 pub request_id: Option<String>,
1345 #[serde(default, skip_serializing_if = "Option::is_none")]
1346 pub api_refusal_category: Option<String>,
1347 #[serde(default, skip_serializing_if = "Option::is_none")]
1352 pub saw_cyber_refusal: Option<bool>,
1353 #[serde(default, skip_serializing_if = "Option::is_none")]
1354 pub api_refusal_explanation: Option<String>,
1355 #[serde(default, skip_serializing_if = "Option::is_none")]
1356 pub retracted_message_uuids: Option<Vec<String>>,
1357 #[serde(default, skip_serializing_if = "Option::is_none")]
1358 pub refused_user_message_uuid: Option<String>,
1359 pub content: Value,
1360 #[serde(default, skip_serializing_if = "Option::is_none")]
1361 pub uuid: Option<String>,
1362 #[serde(default, skip_serializing_if = "Option::is_none")]
1363 pub session_id: Option<String>,
1364}
1365
1366#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1370pub enum RefusalFallbackScope {
1371 Session,
1373 Local,
1376 Unknown(String),
1378}
1379
1380impl RefusalFallbackScope {
1381 pub fn as_str(&self) -> &str {
1382 match self {
1383 Self::Session => "session",
1384 Self::Local => "local",
1385 Self::Unknown(s) => s.as_str(),
1386 }
1387 }
1388}
1389
1390impl fmt::Display for RefusalFallbackScope {
1391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1392 f.write_str(self.as_str())
1393 }
1394}
1395
1396impl From<&str> for RefusalFallbackScope {
1397 fn from(s: &str) -> Self {
1398 match s {
1399 "session" => Self::Session,
1400 "local" => Self::Local,
1401 other => Self::Unknown(other.to_string()),
1402 }
1403 }
1404}
1405
1406impl Serialize for RefusalFallbackScope {
1407 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1408 serializer.serialize_str(self.as_str())
1409 }
1410}
1411
1412impl<'de> Deserialize<'de> for RefusalFallbackScope {
1413 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1414 let s = String::deserialize(deserializer)?;
1415 Ok(Self::from(s.as_str()))
1416 }
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct ModelRefusalNoFallbackMessage {
1421 pub original_model: String,
1422 pub request_id: Option<String>,
1423 #[serde(default, skip_serializing_if = "Option::is_none")]
1424 pub api_refusal_category: Option<String>,
1425 #[serde(default, skip_serializing_if = "Option::is_none")]
1426 pub api_refusal_explanation: Option<String>,
1427 pub content: Value,
1428 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub uuid: Option<String>,
1430 #[serde(default, skip_serializing_if = "Option::is_none")]
1431 pub session_id: Option<String>,
1432}
1433
1434#[derive(Debug, Clone, Serialize, Deserialize)]
1435pub struct LocalCommandOutputMessage {
1436 pub content: String,
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 HookStartedMessage {
1445 pub hook_id: String,
1446 pub hook_name: String,
1447 pub hook_event: 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 HookProgressMessage {
1456 pub hook_id: String,
1457 pub hook_name: String,
1458 pub hook_event: String,
1459 #[serde(default, skip_serializing_if = "Option::is_none")]
1460 pub stdout: Option<String>,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1462 pub stderr: Option<String>,
1463 #[serde(default, skip_serializing_if = "Option::is_none")]
1464 pub output: Option<String>,
1465 #[serde(default, skip_serializing_if = "Option::is_none")]
1466 pub uuid: Option<String>,
1467 #[serde(default, skip_serializing_if = "Option::is_none")]
1468 pub session_id: Option<String>,
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize)]
1472pub struct HookResponseMessage {
1473 pub hook_id: String,
1474 pub hook_name: String,
1475 pub hook_event: String,
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1477 pub stdout: Option<String>,
1478 #[serde(default, skip_serializing_if = "Option::is_none")]
1479 pub stderr: Option<String>,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub output: Option<String>,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 pub exit_code: Option<i32>,
1484 pub outcome: String,
1485 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 pub uuid: Option<String>,
1487 #[serde(default, skip_serializing_if = "Option::is_none")]
1488 pub session_id: Option<String>,
1489}
1490
1491#[derive(Debug, Clone, Serialize, Deserialize)]
1492pub struct PluginInstallMessage {
1493 pub status: String,
1494 #[serde(default, skip_serializing_if = "Option::is_none")]
1495 pub name: Option<String>,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1497 pub error: Option<String>,
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1499 pub uuid: Option<String>,
1500 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub session_id: Option<String>,
1502}
1503
1504#[derive(Debug, Clone, Serialize, Deserialize)]
1505pub struct BackgroundTasksChangedMessage {
1506 pub tasks: Vec<BackgroundTaskInfo>,
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1508 pub uuid: Option<String>,
1509 #[serde(default, skip_serializing_if = "Option::is_none")]
1510 pub session_id: Option<String>,
1511}
1512
1513#[derive(Debug, Clone, Serialize, Deserialize)]
1514pub struct BackgroundTaskInfo {
1515 pub task_id: String,
1516 pub task_type: String,
1517 pub description: String,
1518 #[serde(default, skip_serializing_if = "Option::is_none")]
1521 pub ambient: Option<bool>,
1522}
1523
1524#[derive(Debug, Clone, Serialize, Deserialize)]
1525pub struct SessionStateChangedMessage {
1526 pub state: String,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1528 pub uuid: Option<String>,
1529 #[serde(default, skip_serializing_if = "Option::is_none")]
1530 pub session_id: Option<String>,
1531}
1532
1533#[derive(Debug, Clone, Serialize, Deserialize)]
1534pub struct WorkerShuttingDownMessage {
1535 pub reason: String,
1536 #[serde(default, skip_serializing_if = "Option::is_none")]
1537 pub uuid: Option<String>,
1538 #[serde(default, skip_serializing_if = "Option::is_none")]
1539 pub session_id: Option<String>,
1540}
1541
1542#[derive(Debug, Clone, Serialize, Deserialize)]
1543pub struct CommandsChangedMessage {
1544 pub commands: Vec<CommandInfo>,
1545 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub uuid: Option<String>,
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1548 pub session_id: Option<String>,
1549}
1550
1551#[derive(Debug, Clone, Serialize, Deserialize)]
1552pub struct CommandInfo {
1553 pub name: String,
1554 pub description: String,
1555 #[serde(rename = "argumentHint")]
1556 pub argument_hint: String,
1557 #[serde(default, skip_serializing_if = "Option::is_none")]
1558 pub aliases: Option<Vec<String>>,
1559}
1560
1561#[derive(Debug, Clone, Serialize, Deserialize)]
1562pub struct NotificationMessage {
1563 pub key: String,
1564 pub text: String,
1565 pub priority: String,
1566 #[serde(default, skip_serializing_if = "Option::is_none")]
1567 pub color: Option<String>,
1568 #[serde(default, skip_serializing_if = "Option::is_none")]
1569 pub timeout_ms: Option<u64>,
1570 #[serde(default, skip_serializing_if = "Option::is_none")]
1571 pub uuid: Option<String>,
1572 #[serde(default, skip_serializing_if = "Option::is_none")]
1573 pub session_id: Option<String>,
1574}
1575
1576#[derive(Debug, Clone, Serialize, Deserialize)]
1577pub struct FilesPersistedMessage {
1578 pub files: Vec<PersistedFile>,
1579 pub failed: Vec<FailedPersistedFile>,
1580 pub processed_at: String,
1581 #[serde(default, skip_serializing_if = "Option::is_none")]
1582 pub uuid: Option<String>,
1583 #[serde(default, skip_serializing_if = "Option::is_none")]
1584 pub session_id: Option<String>,
1585}
1586
1587#[derive(Debug, Clone, Serialize, Deserialize)]
1588pub struct PersistedFile {
1589 pub filename: String,
1590 pub file_id: String,
1591}
1592
1593#[derive(Debug, Clone, Serialize, Deserialize)]
1594pub struct FailedPersistedFile {
1595 pub filename: String,
1596 pub error: String,
1597}
1598
1599#[derive(Debug, Clone, Serialize, Deserialize)]
1600pub struct MemoryRecallMessage {
1601 pub mode: String,
1602 pub memories: Vec<MemoryRecallItem>,
1603 #[serde(default, skip_serializing_if = "Option::is_none")]
1604 pub uuid: Option<String>,
1605 #[serde(default, skip_serializing_if = "Option::is_none")]
1606 pub session_id: Option<String>,
1607}
1608
1609#[derive(Debug, Clone, Serialize, Deserialize)]
1610pub struct MemoryRecallItem {
1611 pub path: String,
1612 pub scope: String,
1613 #[serde(default, skip_serializing_if = "Option::is_none")]
1614 pub content: Option<String>,
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1618pub struct ElicitationCompleteMessage {
1619 pub mcp_server_name: String,
1620 pub elicitation_id: String,
1621 #[serde(default, skip_serializing_if = "Option::is_none")]
1622 pub uuid: Option<String>,
1623 #[serde(default, skip_serializing_if = "Option::is_none")]
1624 pub session_id: Option<String>,
1625}
1626
1627#[derive(Debug, Clone, Serialize, Deserialize)]
1628pub struct PermissionDeniedMessage {
1629 pub tool_name: String,
1630 pub tool_use_id: String,
1631 #[serde(default, skip_serializing_if = "Option::is_none")]
1632 pub agent_id: Option<String>,
1633 #[serde(default, skip_serializing_if = "Option::is_none")]
1634 pub decision_reason_type: Option<String>,
1635 #[serde(default, skip_serializing_if = "Option::is_none")]
1636 pub decision_reason: Option<String>,
1637 pub message: String,
1638 #[serde(default, skip_serializing_if = "Option::is_none")]
1639 pub uuid: Option<String>,
1640 #[serde(default, skip_serializing_if = "Option::is_none")]
1641 pub session_id: Option<String>,
1642}
1643
1644#[derive(Debug, Clone, Serialize, Deserialize)]
1645pub struct MirrorErrorMessage {
1646 pub error: String,
1647 pub key: MirrorErrorKey,
1648 #[serde(default, skip_serializing_if = "Option::is_none")]
1649 pub uuid: Option<String>,
1650 #[serde(default, skip_serializing_if = "Option::is_none")]
1651 pub session_id: Option<String>,
1652}
1653
1654#[derive(Debug, Clone, Serialize, Deserialize)]
1655pub struct MirrorErrorKey {
1656 #[serde(rename = "projectKey")]
1657 pub project_key: String,
1658 #[serde(rename = "sessionId")]
1659 pub session_id: String,
1660 #[serde(default, skip_serializing_if = "Option::is_none")]
1661 pub subpath: Option<String>,
1662}
1663
1664#[derive(Debug, Clone, Serialize, Deserialize)]
1665pub struct InformationalMessage {
1666 pub content: String,
1667 pub level: String,
1668 #[serde(default, skip_serializing_if = "Option::is_none")]
1669 pub tool_use_id: Option<String>,
1670 #[serde(default, skip_serializing_if = "Option::is_none")]
1671 pub prevent_continuation: Option<bool>,
1672 #[serde(default, skip_serializing_if = "Option::is_none")]
1673 pub uuid: Option<String>,
1674 #[serde(default, skip_serializing_if = "Option::is_none")]
1675 pub session_id: Option<String>,
1676}
1677
1678#[derive(Debug, Clone, Serialize, Deserialize)]
1680pub struct PluginInfo {
1681 pub name: String,
1683 pub path: String,
1685 #[serde(skip_serializing_if = "Option::is_none")]
1687 pub source: Option<String>,
1688 #[serde(default, skip_serializing_if = "Option::is_none")]
1690 pub version: Option<String>,
1691}
1692
1693#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1695pub struct PluginDiagnostic {
1696 pub plugin: String,
1697 #[serde(rename = "type")]
1698 pub diagnostic_type: String,
1699 pub message: String,
1700}
1701
1702#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1704pub struct MemoryPaths {
1705 #[serde(default, skip_serializing_if = "Option::is_none")]
1706 pub auto: Option<String>,
1707 #[serde(default, skip_serializing_if = "Option::is_none")]
1708 pub team: Option<String>,
1709 #[serde(flatten)]
1710 pub extra: serde_json::Map<String, Value>,
1711}
1712
1713#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1717pub struct McpServerError {
1718 pub name: String,
1719 #[serde(rename = "type")]
1721 pub error_type: String,
1722 pub message: String,
1723}
1724
1725#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub struct InitMessage {
1728 pub session_id: String,
1730 #[serde(skip_serializing_if = "Option::is_none")]
1732 pub cwd: Option<String>,
1733 #[serde(skip_serializing_if = "Option::is_none")]
1735 pub model: Option<String>,
1736 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1738 pub tools: Vec<String>,
1739 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1741 pub mcp_servers: Vec<Value>,
1742 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1744 pub slash_commands: Vec<String>,
1745 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1748 pub terminal_slash_commands: Vec<String>,
1749 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1751 pub agents: Vec<String>,
1752 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1754 pub plugins: Vec<PluginInfo>,
1755 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1757 pub skills: Vec<Value>,
1758 #[serde(skip_serializing_if = "Option::is_none")]
1760 pub claude_code_version: Option<String>,
1761 #[serde(skip_serializing_if = "Option::is_none")]
1764 pub messaging_socket_path: Option<String>,
1765 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1767 pub api_key_source: Option<ApiKeySource>,
1768 #[serde(skip_serializing_if = "Option::is_none")]
1770 pub output_style: Option<OutputStyle>,
1771 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1773 pub permission_mode: Option<InitPermissionMode>,
1774
1775 #[serde(skip_serializing_if = "Option::is_none")]
1777 pub uuid: Option<String>,
1778
1779 #[serde(skip_serializing_if = "Option::is_none")]
1781 pub memory_paths: Option<MemoryPaths>,
1782
1783 #[serde(skip_serializing_if = "Option::is_none")]
1785 pub fast_mode_state: Option<String>,
1786
1787 #[serde(default, skip_serializing_if = "Option::is_none")]
1789 pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
1790
1791 #[serde(default, skip_serializing_if = "Option::is_none")]
1794 pub mcp_server_errors: Option<Vec<McpServerError>>,
1795
1796 #[serde(default, skip_serializing_if = "Option::is_none")]
1798 pub analytics_disabled: Option<bool>,
1799
1800 #[serde(default, skip_serializing_if = "Option::is_none")]
1802 pub product_feedback_disabled: Option<bool>,
1803
1804 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1806 pub betas: Vec<String>,
1807
1808 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1810 pub capabilities: Vec<String>,
1811
1812 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1814 pub plugin_errors: Vec<PluginDiagnostic>,
1815
1816 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1818 pub plugin_warnings: Vec<PluginDiagnostic>,
1819
1820 #[serde(default, skip_serializing_if = "Option::is_none")]
1825 pub effort: Option<String>,
1826
1827 #[serde(default, skip_serializing_if = "Option::is_none")]
1832 pub cloud_session: Option<Value>,
1833
1834 #[serde(default, skip_serializing_if = "Option::is_none")]
1838 pub footer_indicator: Option<FooterIndicator>,
1839
1840 #[serde(default, skip_serializing_if = "Option::is_none")]
1844 pub worker_epoch: Option<u64>,
1845
1846 #[serde(default, skip_serializing_if = "Option::is_none")]
1850 pub powershell_path: Option<Option<String>>,
1851
1852 #[serde(default, skip_serializing_if = "Option::is_none")]
1856 pub startup_timing: Option<serde_json::Map<String, Value>>,
1857}
1858
1859#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1863pub struct FooterIndicator {
1864 pub text: String,
1867}
1868
1869#[derive(Debug, Clone, Serialize, Deserialize)]
1871pub struct StatusMessage {
1872 pub session_id: String,
1874 pub status: Option<StatusMessageStatus>,
1876 #[serde(skip_serializing_if = "Option::is_none")]
1878 pub uuid: Option<String>,
1879 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1881 pub permission_mode: Option<InitPermissionMode>,
1882 #[serde(skip_serializing_if = "Option::is_none")]
1883 pub compact_result: Option<String>,
1884 #[serde(skip_serializing_if = "Option::is_none")]
1885 pub compact_error: Option<String>,
1886}
1887
1888#[derive(Debug, Clone, Serialize, Deserialize)]
1890pub struct CompactBoundaryMessage {
1891 pub session_id: String,
1893 pub compact_metadata: CompactMetadata,
1895 #[serde(
1899 default,
1900 skip_serializing_if = "Option::is_none",
1901 alias = "content",
1902 alias = "text"
1903 )]
1904 pub summary: Option<String>,
1905 #[serde(
1909 default,
1910 skip_serializing_if = "Option::is_none",
1911 alias = "message_count"
1912 )]
1913 pub leaf_message_count: Option<u32>,
1914 #[serde(default, skip_serializing_if = "Option::is_none")]
1916 pub duration_ms: Option<u64>,
1917 #[serde(skip_serializing_if = "Option::is_none")]
1919 pub uuid: Option<String>,
1920 #[serde(skip_serializing_if = "Option::is_none")]
1922 pub logical_parent_uuid: Option<Option<String>>,
1923 #[serde(default, skip_serializing_if = "Option::is_none")]
1927 pub historical: Option<bool>,
1928}
1929
1930#[derive(Debug, Clone, Serialize, Deserialize)]
1932pub struct CompactMetadata {
1933 pub pre_tokens: u64,
1935 pub trigger: CompactionTrigger,
1937 #[serde(default, skip_serializing_if = "Option::is_none")]
1938 pub post_tokens: Option<u64>,
1939 #[serde(default, skip_serializing_if = "Option::is_none")]
1940 pub cumulative_dropped_tokens: Option<u64>,
1941 #[serde(default, skip_serializing_if = "Option::is_none")]
1942 pub duration_ms: Option<u64>,
1943 #[serde(default, skip_serializing_if = "Option::is_none")]
1944 pub user_context: Option<String>,
1945 #[serde(default, skip_serializing_if = "Option::is_none")]
1946 pub messages_summarized: Option<u64>,
1947 #[serde(default, skip_serializing_if = "Option::is_none")]
1948 pub precomputed: Option<bool>,
1949 #[serde(default, skip_serializing_if = "Option::is_none")]
1950 pub pre_compact_discovered_tools: Option<Vec<String>>,
1951 #[serde(default, skip_serializing_if = "Option::is_none")]
1952 pub preserved_segment: Option<PreservedSegment>,
1953 #[serde(default, skip_serializing_if = "Option::is_none")]
1954 pub preserved_messages: Option<PreservedMessages>,
1955}
1956
1957#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1958pub struct PreservedSegment {
1959 pub head_uuid: String,
1960 pub anchor_uuid: String,
1961 pub tail_uuid: String,
1962}
1963
1964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1965pub struct PreservedMessages {
1966 pub anchor_uuid: String,
1967 pub uuids: Vec<String>,
1968 #[serde(default, skip_serializing_if = "Option::is_none")]
1969 pub all_uuids: Option<Vec<String>>,
1970}
1971
1972#[derive(Debug, Clone, Serialize, Deserialize)]
1978pub struct TaskUsage {
1979 pub duration_ms: u64,
1981 pub tool_uses: u64,
1983 pub total_tokens: u64,
1985}
1986
1987#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1989pub enum TaskType {
1990 LocalAgent,
1992 LocalBash,
1994 LocalWorkflow,
1996 Unknown(String),
1998}
1999
2000impl TaskType {
2001 pub fn as_str(&self) -> &str {
2002 match self {
2003 Self::LocalAgent => "local_agent",
2004 Self::LocalBash => "local_bash",
2005 Self::LocalWorkflow => "local_workflow",
2006 Self::Unknown(s) => s.as_str(),
2007 }
2008 }
2009}
2010
2011impl fmt::Display for TaskType {
2012 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2013 f.write_str(self.as_str())
2014 }
2015}
2016
2017impl From<&str> for TaskType {
2018 fn from(s: &str) -> Self {
2019 match s {
2020 "local_agent" => Self::LocalAgent,
2021 "local_bash" => Self::LocalBash,
2022 "local_workflow" => Self::LocalWorkflow,
2023 other => Self::Unknown(other.to_string()),
2024 }
2025 }
2026}
2027
2028impl Serialize for TaskType {
2029 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2030 serializer.serialize_str(self.as_str())
2031 }
2032}
2033
2034impl<'de> Deserialize<'de> for TaskType {
2035 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2036 let s = String::deserialize(deserializer)?;
2037 Ok(Self::from(s.as_str()))
2038 }
2039}
2040
2041#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2043pub enum TaskStatus {
2044 Pending,
2045 Running,
2046 Completed,
2047 Failed,
2048 Killed,
2049 Paused,
2050 Stopped,
2051 Unknown(String),
2052}
2053
2054impl TaskStatus {
2055 pub fn as_str(&self) -> &str {
2056 match self {
2057 Self::Pending => "pending",
2058 Self::Running => "running",
2059 Self::Completed => "completed",
2060 Self::Failed => "failed",
2061 Self::Killed => "killed",
2062 Self::Paused => "paused",
2063 Self::Stopped => "stopped",
2064 Self::Unknown(s) => s.as_str(),
2065 }
2066 }
2067}
2068
2069impl fmt::Display for TaskStatus {
2070 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2071 f.write_str(self.as_str())
2072 }
2073}
2074
2075impl From<&str> for TaskStatus {
2076 fn from(s: &str) -> Self {
2077 match s {
2078 "pending" => Self::Pending,
2079 "running" => Self::Running,
2080 "completed" => Self::Completed,
2081 "failed" => Self::Failed,
2082 "killed" => Self::Killed,
2083 "paused" => Self::Paused,
2084 "stopped" => Self::Stopped,
2085 other => Self::Unknown(other.to_string()),
2086 }
2087 }
2088}
2089
2090impl Serialize for TaskStatus {
2091 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2092 serializer.serialize_str(self.as_str())
2093 }
2094}
2095
2096impl<'de> Deserialize<'de> for TaskStatus {
2097 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2098 let s = String::deserialize(deserializer)?;
2099 Ok(Self::from(s.as_str()))
2100 }
2101}
2102
2103#[derive(Debug, Clone, Serialize, Deserialize)]
2105pub struct TaskStartedMessage {
2106 pub session_id: String,
2107 pub task_id: String,
2108 #[serde(default, skip_serializing_if = "Option::is_none")]
2109 pub task_type: Option<TaskType>,
2110 #[serde(default, skip_serializing_if = "Option::is_none")]
2111 pub tool_use_id: Option<String>,
2112 pub description: String,
2113 #[serde(default, skip_serializing_if = "Option::is_none")]
2116 pub subagent_type: Option<String>,
2117 #[serde(default, skip_serializing_if = "Option::is_none")]
2123 pub is_backgrounded: Option<bool>,
2124 #[serde(default, skip_serializing_if = "Option::is_none")]
2128 pub spawn_depth: Option<u32>,
2129 #[serde(default, skip_serializing_if = "Option::is_none")]
2131 pub prompt: Option<String>,
2132 #[serde(default, skip_serializing_if = "Option::is_none")]
2133 pub workflow_name: Option<String>,
2134 #[serde(default, skip_serializing_if = "Option::is_none")]
2135 pub skip_transcript: Option<bool>,
2136 #[serde(default, skip_serializing_if = "Option::is_none")]
2141 pub ambient: Option<bool>,
2142 pub uuid: String,
2143}
2144
2145#[derive(Debug, Clone, Serialize, Deserialize)]
2149pub struct TaskUpdatedMessage {
2150 pub session_id: String,
2151 pub task_id: String,
2152 pub patch: TaskPatch,
2153 pub uuid: String,
2154}
2155
2156#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2159pub struct TaskPatch {
2160 #[serde(default, skip_serializing_if = "Option::is_none")]
2161 pub status: Option<TaskStatus>,
2162 #[serde(default, skip_serializing_if = "Option::is_none")]
2165 pub end_time: Option<u64>,
2166 #[serde(default, skip_serializing_if = "Option::is_none")]
2167 pub description: Option<String>,
2168 #[serde(default, skip_serializing_if = "Option::is_none")]
2169 pub total_paused_ms: Option<u64>,
2170 #[serde(default, skip_serializing_if = "Option::is_none")]
2171 pub error: Option<String>,
2172 #[serde(default, skip_serializing_if = "Option::is_none")]
2173 pub is_backgrounded: Option<bool>,
2174}
2175
2176#[derive(Debug, Clone, Serialize, Deserialize)]
2179pub struct ThinkingTokensMessage {
2180 pub session_id: String,
2181 pub estimated_tokens: u64,
2183 pub estimated_tokens_delta: u64,
2185 #[serde(default, skip_serializing_if = "Option::is_none")]
2192 pub user_message_uuid: Option<String>,
2193 pub uuid: String,
2194}
2195
2196#[derive(Debug, Clone, Serialize, Deserialize)]
2199pub struct TaskProgressMessage {
2200 pub session_id: String,
2201 pub task_id: String,
2202 #[serde(default, skip_serializing_if = "Option::is_none")]
2203 pub tool_use_id: Option<String>,
2204 pub description: String,
2205 #[serde(default, skip_serializing_if = "Option::is_none")]
2206 pub last_tool_name: Option<String>,
2207 pub usage: TaskUsage,
2208 #[serde(default, skip_serializing_if = "Option::is_none")]
2210 pub subagent_type: Option<String>,
2211 #[serde(default, skip_serializing_if = "Option::is_none")]
2212 pub summary: Option<String>,
2213 pub uuid: String,
2214}
2215
2216#[derive(Debug, Clone, Serialize, Deserialize)]
2219pub struct TaskNotificationMessage {
2220 pub session_id: String,
2221 pub task_id: String,
2222 pub status: TaskStatus,
2223 pub summary: String,
2224 pub output_file: Option<String>,
2225 #[serde(skip_serializing_if = "Option::is_none")]
2226 pub tool_use_id: Option<String>,
2227 #[serde(skip_serializing_if = "Option::is_none")]
2228 pub usage: Option<TaskUsage>,
2229 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2235 pub resource_links: Vec<ResourceLink>,
2236 #[serde(default, skip_serializing_if = "Option::is_none")]
2237 pub skip_transcript: Option<bool>,
2238 #[serde(default, skip_serializing_if = "Option::is_none")]
2241 pub ambient: Option<bool>,
2242 #[serde(skip_serializing_if = "Option::is_none")]
2243 pub uuid: Option<String>,
2244}
2245
2246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2248pub enum AssistantErrorKind {
2249 AuthenticationFailed,
2250 OauthOrgNotAllowed,
2251 AccountOnHold,
2252 BillingError,
2253 RateLimit,
2254 Overloaded,
2255 InvalidRequest,
2256 ModelNotFound,
2257 ServerError,
2258 UnknownError,
2259 MaxOutputTokens,
2260 Unknown(String),
2261}
2262
2263impl AssistantErrorKind {
2264 pub fn as_str(&self) -> &str {
2265 match self {
2266 Self::AuthenticationFailed => "authentication_failed",
2267 Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
2268 Self::AccountOnHold => "account_on_hold",
2269 Self::BillingError => "billing_error",
2270 Self::RateLimit => "rate_limit",
2271 Self::Overloaded => "overloaded",
2272 Self::InvalidRequest => "invalid_request",
2273 Self::ModelNotFound => "model_not_found",
2274 Self::ServerError => "server_error",
2275 Self::UnknownError => "unknown",
2276 Self::MaxOutputTokens => "max_output_tokens",
2277 Self::Unknown(s) => s.as_str(),
2278 }
2279 }
2280}
2281
2282impl fmt::Display for AssistantErrorKind {
2283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2284 f.write_str(self.as_str())
2285 }
2286}
2287
2288impl From<&str> for AssistantErrorKind {
2289 fn from(s: &str) -> Self {
2290 match s {
2291 "authentication_failed" => Self::AuthenticationFailed,
2292 "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
2293 "account_on_hold" => Self::AccountOnHold,
2294 "billing_error" => Self::BillingError,
2295 "rate_limit" => Self::RateLimit,
2296 "overloaded" => Self::Overloaded,
2297 "invalid_request" => Self::InvalidRequest,
2298 "model_not_found" => Self::ModelNotFound,
2299 "server_error" => Self::ServerError,
2300 "unknown" => Self::UnknownError,
2301 "max_output_tokens" => Self::MaxOutputTokens,
2302 other => Self::Unknown(other.to_string()),
2303 }
2304 }
2305}
2306
2307impl Serialize for AssistantErrorKind {
2308 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2309 serializer.serialize_str(self.as_str())
2310 }
2311}
2312
2313impl<'de> Deserialize<'de> for AssistantErrorKind {
2314 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2315 let s = String::deserialize(deserializer)?;
2316 Ok(Self::from(s.as_str()))
2317 }
2318}
2319
2320#[derive(Debug, Clone, Serialize, Deserialize)]
2327pub struct CodeChangePublishedMessage {
2328 pub provider: String,
2332 pub url: String,
2334 pub repo: String,
2337 pub identifier: String,
2339 #[serde(default, skip_serializing_if = "Option::is_none")]
2348 pub action: Option<String>,
2349 #[serde(default, skip_serializing_if = "Option::is_none")]
2354 pub branch: Option<String>,
2355 pub uuid: String,
2356 pub session_id: String,
2357}
2358
2359#[derive(Debug, Clone, Serialize, Deserialize)]
2364pub struct VcsStateChangedMessage {
2365 pub kind: VcsMutationKind,
2368 pub cwd: String,
2371 #[serde(default, skip_serializing_if = "Option::is_none")]
2376 pub branch: Option<String>,
2377 pub uuid: String,
2378 pub session_id: String,
2379}
2380
2381#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2383pub enum VcsMutationKind {
2384 Commit,
2385 Push,
2386 Merge,
2387 Rebase,
2388 Unknown(String),
2390}
2391
2392impl VcsMutationKind {
2393 pub fn as_str(&self) -> &str {
2394 match self {
2395 Self::Commit => "commit",
2396 Self::Push => "push",
2397 Self::Merge => "merge",
2398 Self::Rebase => "rebase",
2399 Self::Unknown(s) => s.as_str(),
2400 }
2401 }
2402}
2403
2404impl fmt::Display for VcsMutationKind {
2405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2406 f.write_str(self.as_str())
2407 }
2408}
2409
2410impl From<&str> for VcsMutationKind {
2411 fn from(s: &str) -> Self {
2412 match s {
2413 "commit" => Self::Commit,
2414 "push" => Self::Push,
2415 "merge" => Self::Merge,
2416 "rebase" => Self::Rebase,
2417 other => Self::Unknown(other.to_string()),
2418 }
2419 }
2420}
2421
2422impl Serialize for VcsMutationKind {
2423 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2424 serializer.serialize_str(self.as_str())
2425 }
2426}
2427
2428impl<'de> Deserialize<'de> for VcsMutationKind {
2429 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2430 let s = String::deserialize(deserializer)?;
2431 Ok(Self::from(s.as_str()))
2432 }
2433}
2434
2435#[derive(Debug, Clone, Serialize, Deserialize)]
2437pub struct FeedbackDraftQueuedMessage {
2438 pub draft_id: String,
2439 pub draft_type: String,
2440 pub title: String,
2441 pub details_preview: String,
2442 #[serde(default, skip_serializing_if = "Option::is_none")]
2443 pub uuid: Option<String>,
2444 #[serde(default, skip_serializing_if = "Option::is_none")]
2445 pub session_id: Option<String>,
2446 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2447 pub extra: serde_json::Map<String, Value>,
2448}
2449
2450#[derive(Debug, Clone, Serialize, Deserialize)]
2458pub struct CloudSessionDeltaMessage {
2459 pub seq: u64,
2463 pub changed: Vec<String>,
2467 pub cloud_session: Value,
2471 pub uuid: String,
2472 pub session_id: String,
2473 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2474 pub extra: serde_json::Map<String, Value>,
2475}
2476
2477#[derive(Debug, Clone, Serialize, Deserialize)]
2487pub struct DevIntentMessage {
2488 pub kind: DevIntentKind,
2490 pub uuid: String,
2491 pub session_id: String,
2492 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2493 pub extra: serde_json::Map<String, Value>,
2494}
2495
2496#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2500pub enum DevIntentKind {
2501 IosApp,
2505 Unknown(String),
2507}
2508
2509impl DevIntentKind {
2510 pub fn as_str(&self) -> &str {
2511 match self {
2512 Self::IosApp => "ios_app",
2513 Self::Unknown(s) => s.as_str(),
2514 }
2515 }
2516}
2517
2518impl fmt::Display for DevIntentKind {
2519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2520 f.write_str(self.as_str())
2521 }
2522}
2523
2524impl From<&str> for DevIntentKind {
2525 fn from(s: &str) -> Self {
2526 match s {
2527 "ios_app" => Self::IosApp,
2528 other => Self::Unknown(other.to_string()),
2529 }
2530 }
2531}
2532
2533impl Serialize for DevIntentKind {
2534 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2535 serializer.serialize_str(self.as_str())
2536 }
2537}
2538
2539impl<'de> Deserialize<'de> for DevIntentKind {
2540 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2541 let s = String::deserialize(deserializer)?;
2542 Ok(Self::from(s.as_str()))
2543 }
2544}
2545
2546#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2549pub struct BatchToolUse {
2550 pub id: String,
2551 pub name: String,
2552}
2553
2554#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2559pub struct ContextUsage {
2560 pub model: String,
2562 pub total_tokens: u64,
2565 pub raw_max_tokens: u64,
2568 pub percentage: u64,
2570 #[serde(default, skip_serializing_if = "Option::is_none")]
2572 pub over_limit: Option<ContextOverLimit>,
2573 #[serde(default)]
2575 pub categories: Vec<ContextCategory>,
2576 #[serde(default)]
2578 pub mcp_tools: Vec<ContextMcpTool>,
2579 #[serde(default)]
2581 pub memory_files: Vec<ContextMemoryFile>,
2582 #[serde(default)]
2584 pub agents: Vec<ContextAgent>,
2585 #[serde(default, skip_serializing_if = "Option::is_none")]
2587 pub skills: Option<Vec<ContextSkill>>,
2588}
2589
2590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2592pub struct ContextOverLimit {
2593 pub tokens_over: u64,
2594 pub kind: String,
2597}
2598
2599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2601pub struct ContextCategory {
2602 pub name: String,
2605 pub tokens: u64,
2606 pub kind: String,
2610}
2611
2612#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2614pub struct ContextMcpTool {
2615 pub name: String,
2617 pub server_name: String,
2618 pub tokens: u64,
2619}
2620
2621#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2623pub struct ContextMemoryFile {
2624 pub path: String,
2625 #[serde(rename = "type")]
2627 pub file_type: String,
2628 pub tokens: u64,
2629}
2630
2631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2633pub struct ContextAgent {
2634 pub agent_type: String,
2635 pub source: String,
2637 pub tokens: u64,
2638}
2639
2640#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2642pub struct ContextSkill {
2643 pub name: String,
2644 pub source: String,
2646 #[serde(default, skip_serializing_if = "Option::is_none")]
2647 pub plugin_name: Option<String>,
2648 pub tokens: u64,
2649}
2650
2651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2653pub struct ToolUseMeta {
2654 pub id: String,
2655 pub display_name: String,
2656 #[serde(default, skip_serializing_if = "Option::is_none")]
2657 pub server_display_name: Option<String>,
2658 #[serde(default, skip_serializing_if = "Option::is_none")]
2659 pub icon_url: Option<String>,
2660}
2661
2662#[derive(Debug, Clone, Serialize, Deserialize)]
2664pub struct AssistantMessage {
2665 pub message: AssistantMessageContent,
2666 #[serde(alias = "sessionId")]
2667 pub session_id: String,
2668 #[serde(skip_serializing_if = "Option::is_none")]
2669 pub uuid: Option<String>,
2670 #[serde(skip_serializing_if = "Option::is_none")]
2671 pub parent_tool_use_id: Option<String>,
2672 #[serde(skip_serializing_if = "Option::is_none")]
2674 pub request_id: Option<String>,
2675 #[serde(default, skip_serializing_if = "Option::is_none")]
2684 pub user_message_uuid: Option<String>,
2685 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2691 pub user_message_uuids: Vec<String>,
2692 #[serde(skip_serializing_if = "Option::is_none")]
2695 pub subagent_type: Option<String>,
2696 #[serde(skip_serializing_if = "Option::is_none")]
2698 pub task_description: Option<String>,
2699 #[serde(skip_serializing_if = "Option::is_none")]
2700 pub error: Option<AssistantErrorKind>,
2701 #[serde(default, skip_serializing_if = "Option::is_none")]
2705 pub aborted: Option<bool>,
2706 #[serde(default, skip_serializing_if = "Option::is_none")]
2711 pub resumed_from_incomplete_thinking: Option<bool>,
2712 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2713 pub supersedes: Vec<String>,
2714 #[serde(skip_serializing_if = "Option::is_none")]
2715 pub timestamp: Option<String>,
2716 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2717 pub tool_use_meta: Vec<ToolUseMeta>,
2718 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2723 pub batch_tool_uses: Vec<BatchToolUse>,
2724 #[serde(default, skip_serializing_if = "Option::is_none")]
2730 pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
2731 #[serde(default, skip_serializing_if = "Option::is_none")]
2737 pub wire_ingest_context: Option<serde_json::Map<String, Value>>,
2738 #[serde(default, skip_serializing_if = "Option::is_none")]
2742 pub historical: Option<bool>,
2743 #[serde(default, skip_serializing_if = "Option::is_none")]
2751 pub resume_reason: Option<String>,
2752 #[serde(default, skip_serializing_if = "Option::is_none")]
2758 pub local_command_source: Option<String>,
2759 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2768 pub narration_block_indexes: Vec<usize>,
2769 #[serde(default, skip_serializing_if = "Option::is_none")]
2773 pub context_usage: Option<ContextUsage>,
2774 #[serde(default, skip_serializing_if = "Option::is_none")]
2775 pub is_meta: Option<bool>,
2776 #[serde(default, skip_serializing_if = "Option::is_none")]
2777 pub is_virtual: Option<bool>,
2778 #[serde(default, skip_serializing_if = "Option::is_none")]
2779 pub is_api_error_message: Option<bool>,
2780 #[serde(skip_serializing_if = "Option::is_none")]
2781 pub api_error_status: Option<u16>,
2782 #[serde(skip_serializing_if = "Option::is_none")]
2783 pub api_error: Option<String>,
2784 #[serde(skip_serializing_if = "Option::is_none")]
2785 pub error_details: Option<String>,
2786 #[serde(skip_serializing_if = "Option::is_none")]
2787 pub advisor_model: Option<String>,
2788 #[serde(skip_serializing_if = "Option::is_none")]
2789 pub attribution_agent: Option<String>,
2790 #[serde(skip_serializing_if = "Option::is_none")]
2791 pub attribution_skill: Option<String>,
2792 #[serde(skip_serializing_if = "Option::is_none")]
2793 pub attribution_plugin: Option<String>,
2794 #[serde(skip_serializing_if = "Option::is_none")]
2795 pub attribution_mcp_server: Option<String>,
2796 #[serde(skip_serializing_if = "Option::is_none")]
2797 pub attribution_mcp_tool: Option<String>,
2798}
2799
2800#[derive(Debug, Clone, Serialize, Deserialize)]
2802pub struct AssistantMessageContent {
2803 pub id: String,
2804 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2806 pub message_type: Option<String>,
2807 pub role: MessageRole,
2808 pub model: String,
2809 pub content: Vec<ContentBlock>,
2810 #[serde(skip_serializing_if = "Option::is_none")]
2811 pub stop_reason: Option<StopReason>,
2812 #[serde(skip_serializing_if = "Option::is_none")]
2813 pub stop_sequence: Option<String>,
2814 #[serde(skip_serializing_if = "Option::is_none")]
2815 pub usage: Option<AssistantUsage>,
2816 #[serde(skip_serializing_if = "Option::is_none")]
2818 pub stop_details: Option<Value>,
2819 #[serde(skip_serializing_if = "Option::is_none")]
2821 pub context_management: Option<Value>,
2822}
2823
2824#[derive(Debug, Clone, Serialize, Deserialize)]
2826pub struct AssistantUsage {
2827 #[serde(default)]
2829 pub input_tokens: u32,
2830
2831 #[serde(default)]
2833 pub output_tokens: u32,
2834
2835 #[serde(default)]
2837 pub cache_creation_input_tokens: u32,
2838
2839 #[serde(default)]
2841 pub cache_read_input_tokens: u32,
2842
2843 #[serde(skip_serializing_if = "Option::is_none")]
2845 pub service_tier: Option<String>,
2846
2847 #[serde(skip_serializing_if = "Option::is_none")]
2849 pub cache_creation: Option<CacheCreationDetails>,
2850
2851 #[serde(skip_serializing_if = "Option::is_none")]
2853 pub inference_geo: Option<String>,
2854}
2855
2856#[derive(Debug, Clone, Serialize, Deserialize)]
2858pub struct CacheCreationDetails {
2859 #[serde(default)]
2861 pub ephemeral_1h_input_tokens: u32,
2862
2863 #[serde(default)]
2865 pub ephemeral_5m_input_tokens: u32,
2866}
2867
2868#[cfg(test)]
2869mod tests {
2870 use crate::io::ClaudeOutput;
2871
2872 #[test]
2873 fn test_subagent_usage_rollup_accumulates_task_results() {
2874 use super::SubagentUsageRollup;
2875
2876 let mut rollup = SubagentUsageRollup::default();
2877
2878 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}}"#;
2879 let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2880 assert!(rollup.observe(&output));
2881 assert_eq!(rollup.subagent_tokens, 10201);
2882 assert_eq!(rollup.agent_count, 1);
2883 assert_eq!(rollup.tool_uses, 3);
2884 assert_eq!(rollup.duration_ms, 1853);
2885
2886 assert!(!rollup.observe(&output));
2888 assert_eq!(rollup.agent_count, 1);
2889 assert_eq!(rollup.subagent_tokens, 10201);
2890
2891 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}}"#;
2893 let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2894 assert!(rollup.observe(&output));
2895 assert_eq!(rollup.agent_count, 2);
2896 assert_eq!(rollup.subagent_tokens, 10701);
2897 }
2898
2899 #[test]
2900 fn test_subagent_usage_rollup_ignores_non_task_results() {
2901 use super::SubagentUsageRollup;
2902
2903 let mut rollup = SubagentUsageRollup::default();
2904
2905 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}}"#;
2908 let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2909 assert!(!rollup.observe(&output));
2910
2911 let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2913 let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2914 assert!(!rollup.observe(&output));
2915
2916 let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2918 let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2919 assert!(!rollup.observe(&output));
2920
2921 assert_eq!(rollup, SubagentUsageRollup::default());
2922 }
2923
2924 #[test]
2925 fn test_subagent_usage_rollup_over_captured_session() {
2926 use super::SubagentUsageRollup;
2927
2928 let mut rollup = SubagentUsageRollup::default();
2929 let fixture =
2930 include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2931 for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2932 if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2933 rollup.observe(&output);
2934 }
2935 }
2936 assert_eq!(rollup.agent_count, 1);
2937 assert_eq!(rollup.subagent_tokens, 10201);
2938 }
2939
2940 #[test]
2941 fn test_system_message_init() {
2942 let json = r#"{
2943 "type": "system",
2944 "subtype": "init",
2945 "session_id": "test-session-123",
2946 "cwd": "/home/user/project",
2947 "model": "claude-sonnet-4",
2948 "tools": ["Bash", "Read", "Write"],
2949 "mcp_servers": [],
2950 "slash_commands": ["compact", "cost", "review"],
2951 "agents": ["Bash", "Explore", "Plan"],
2952 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2953 "skills": [],
2954 "claude_code_version": "2.1.15",
2955 "apiKeySource": "none",
2956 "output_style": "default",
2957 "permissionMode": "default"
2958 }"#;
2959
2960 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2961 if let ClaudeOutput::System(sys) = output {
2962 assert!(sys.is_init());
2963 assert!(!sys.is_status());
2964 assert!(!sys.is_compact_boundary());
2965
2966 let init = sys.as_init().expect("Should parse as init");
2967 assert_eq!(init.session_id, "test-session-123");
2968 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2969 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2970 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2971 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2972 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2973 assert_eq!(init.plugins.len(), 1);
2974 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2975 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2976 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2977 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2978 assert_eq!(
2979 init.permission_mode,
2980 Some(super::InitPermissionMode::Default)
2981 );
2982 } else {
2983 panic!("Expected System message");
2984 }
2985 }
2986
2987 #[test]
2988 fn test_system_message_init_from_real_capture() {
2989 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2990 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2991 if let ClaudeOutput::System(sys) = output {
2992 let init = sys.as_init().expect("Should parse real init capture");
2993 assert_eq!(init.slash_commands.len(), 8);
2994 assert!(init.slash_commands.contains(&"compact".to_string()));
2995 assert!(init.slash_commands.contains(&"review".to_string()));
2996 assert_eq!(init.agents.len(), 5);
2997 assert!(init.agents.contains(&"Bash".to_string()));
2998 assert!(init.agents.contains(&"Explore".to_string()));
2999 assert_eq!(init.plugins.len(), 1);
3000 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
3001 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
3002 } else {
3003 panic!("Expected System message");
3004 }
3005 }
3006
3007 #[test]
3008 fn test_system_message_status() {
3009 let json = r#"{
3010 "type": "system",
3011 "subtype": "status",
3012 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3013 "status": "compacting",
3014 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
3015 }"#;
3016
3017 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3018 if let ClaudeOutput::System(sys) = output {
3019 assert!(sys.is_status());
3020 assert!(!sys.is_init());
3021
3022 let status = sys.as_status().expect("Should parse as status");
3023 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3024 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
3025 assert_eq!(
3026 status.uuid,
3027 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
3028 );
3029 } else {
3030 panic!("Expected System message");
3031 }
3032 }
3033
3034 #[test]
3035 fn test_system_message_status_null() {
3036 let json = r#"{
3037 "type": "system",
3038 "subtype": "status",
3039 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3040 "status": null,
3041 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
3042 }"#;
3043
3044 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3045 if let ClaudeOutput::System(sys) = output {
3046 let status = sys.as_status().expect("Should parse as status");
3047 assert_eq!(status.status, None);
3048 } else {
3049 panic!("Expected System message");
3050 }
3051 }
3052
3053 #[test]
3054 fn test_system_message_task_started() {
3055 let json = r#"{
3056 "type": "system",
3057 "subtype": "task_started",
3058 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3059 "task_id": "b6daf3f",
3060 "task_type": "local_bash",
3061 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
3062 "description": "Wait for CI on PR #12",
3063 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
3064 }"#;
3065
3066 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3067 if let ClaudeOutput::System(sys) = output {
3068 assert!(sys.is_task_started());
3069 assert!(!sys.is_task_progress());
3070 assert!(!sys.is_task_notification());
3071
3072 let task = sys.as_task_started().expect("Should parse as task_started");
3073 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
3074 assert_eq!(task.task_id, "b6daf3f");
3075 assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
3076 assert_eq!(
3077 task.tool_use_id.as_deref(),
3078 Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
3079 );
3080 assert_eq!(task.description, "Wait for CI on PR #12");
3081 } else {
3082 panic!("Expected System message");
3083 }
3084 }
3085
3086 #[test]
3087 fn test_system_message_task_started_agent() {
3088 let json = r#"{
3089 "type": "system",
3090 "subtype": "task_started",
3091 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3092 "task_id": "a4a7e0906e5fc64cc",
3093 "task_type": "local_agent",
3094 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3095 "description": "Explore Scene/ArrayScene duplication",
3096 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3097 }"#;
3098
3099 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3100 if let ClaudeOutput::System(sys) = output {
3101 let task = sys.as_task_started().expect("Should parse as task_started");
3102 assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
3103 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
3104 } else {
3105 panic!("Expected System message");
3106 }
3107 }
3108
3109 #[test]
3110 fn test_system_message_task_progress() {
3111 let json = r#"{
3112 "type": "system",
3113 "subtype": "task_progress",
3114 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3115 "task_id": "a4a7e0906e5fc64cc",
3116 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3117 "description": "Reading src/jplephem/chebyshev.rs",
3118 "last_tool_name": "Read",
3119 "usage": {
3120 "duration_ms": 13996,
3121 "tool_uses": 9,
3122 "total_tokens": 38779
3123 },
3124 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3125 }"#;
3126
3127 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3128 if let ClaudeOutput::System(sys) = output {
3129 assert!(sys.is_task_progress());
3130 assert!(!sys.is_task_started());
3131
3132 let progress = sys
3133 .as_task_progress()
3134 .expect("Should parse as task_progress");
3135 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
3136 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
3137 assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
3138 assert_eq!(progress.usage.duration_ms, 13996);
3139 assert_eq!(progress.usage.tool_uses, 9);
3140 assert_eq!(progress.usage.total_tokens, 38779);
3141 } else {
3142 panic!("Expected System message");
3143 }
3144 }
3145
3146 #[test]
3147 fn test_system_message_task_notification_completed() {
3148 let json = r#"{
3149 "type": "system",
3150 "subtype": "task_notification",
3151 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3152 "task_id": "a0ba761e9dc9c316f",
3153 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
3154 "status": "completed",
3155 "summary": "Agent \"Write Hipparcos data source doc\" completed",
3156 "output_file": "",
3157 "usage": {
3158 "duration_ms": 172300,
3159 "tool_uses": 11,
3160 "total_tokens": 42005
3161 },
3162 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
3163 }"#;
3164
3165 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3166 if let ClaudeOutput::System(sys) = output {
3167 assert!(sys.is_task_notification());
3168
3169 let notif = sys
3170 .as_task_notification()
3171 .expect("Should parse as task_notification");
3172 assert_eq!(notif.status, super::TaskStatus::Completed);
3173 assert_eq!(
3174 notif.summary,
3175 "Agent \"Write Hipparcos data source doc\" completed"
3176 );
3177 assert_eq!(notif.output_file, Some("".to_string()));
3178 assert_eq!(
3179 notif.tool_use_id,
3180 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
3181 );
3182 let usage = notif.usage.expect("Should have usage");
3183 assert_eq!(usage.duration_ms, 172300);
3184 assert_eq!(usage.tool_uses, 11);
3185 assert_eq!(usage.total_tokens, 42005);
3186 } else {
3187 panic!("Expected System message");
3188 }
3189 }
3190
3191 #[test]
3192 fn test_system_message_task_notification_failed_no_usage() {
3193 let json = r#"{
3194 "type": "system",
3195 "subtype": "task_notification",
3196 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
3197 "task_id": "b98f6a3",
3198 "status": "failed",
3199 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
3200 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
3201 }"#;
3202
3203 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3204 if let ClaudeOutput::System(sys) = output {
3205 let notif = sys
3206 .as_task_notification()
3207 .expect("Should parse as task_notification");
3208 assert_eq!(notif.status, super::TaskStatus::Failed);
3209 assert!(notif.tool_use_id.is_none());
3210 assert!(notif.usage.is_none());
3211 assert_eq!(
3212 notif.output_file,
3213 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
3214 );
3215 } else {
3216 panic!("Expected System message");
3217 }
3218 }
3219
3220 #[test]
3226 fn test_task_messages_roundtrip_through_value() {
3227 let cases = [
3228 r#"{"type":"system","subtype":"task_started","session_id":"s1",
3229 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
3230 "description":"Sleep 3s","uuid":"u1"}"#,
3231 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
3232 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
3233 "last_tool_name":"Bash",
3234 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3235 "uuid":"u2"}"#,
3236 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
3237 "task_id":"t1","tool_use_id":"tu1","status":"completed",
3238 "summary":"done","output_file":"",
3239 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3240 "uuid":"u3"}"#,
3241 ];
3242
3243 for json in cases {
3244 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3245 let value = serde_json::to_value(&output).unwrap();
3246 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
3247
3248 let ClaudeOutput::System(sys) = reparsed else {
3249 panic!("Expected System variant after round-trip");
3250 };
3251
3252 match sys.subtype {
3253 super::SystemSubtype::TaskStarted => {
3254 assert!(
3255 sys.as_task_started().is_some(),
3256 "as_task_started failed after round-trip"
3257 );
3258 }
3259 super::SystemSubtype::TaskProgress => {
3260 assert!(
3261 sys.as_task_progress().is_some(),
3262 "as_task_progress failed after round-trip"
3263 );
3264 }
3265 super::SystemSubtype::TaskNotification => {
3266 assert!(
3267 sys.as_task_notification().is_some(),
3268 "as_task_notification failed after round-trip"
3269 );
3270 }
3271 other => panic!("unexpected subtype after round-trip: {other:?}"),
3272 }
3273 }
3274 }
3275
3276 #[test]
3277 fn test_system_message_compact_boundary() {
3278 let json = r#"{
3279 "type": "system",
3280 "subtype": "compact_boundary",
3281 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3282 "compact_metadata": {
3283 "pre_tokens": 155285,
3284 "trigger": "auto"
3285 },
3286 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
3287 }"#;
3288
3289 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3290 if let ClaudeOutput::System(sys) = output {
3291 assert!(sys.is_compact_boundary());
3292 assert!(!sys.is_init());
3293 assert!(!sys.is_status());
3294
3295 let compact = sys
3296 .as_compact_boundary()
3297 .expect("Should parse as compact_boundary");
3298 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3299 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
3300 assert_eq!(
3301 compact.compact_metadata.trigger,
3302 super::CompactionTrigger::Auto
3303 );
3304 assert!(compact.summary.is_none());
3306 assert!(compact.leaf_message_count.is_none());
3307 assert!(compact.duration_ms.is_none());
3308 } else {
3309 panic!("Expected System message");
3310 }
3311 }
3312
3313 #[test]
3314 fn test_compact_boundary_with_summary_stats() {
3315 let json = r#"{
3317 "type": "system",
3318 "subtype": "compact_boundary",
3319 "session_id": "s1",
3320 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
3321 "summary": "Summarized the earlier exploration.",
3322 "leaf_message_count": 42,
3323 "duration_ms": 1234,
3324 "uuid": "u1"
3325 }"#;
3326 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3327 let ClaudeOutput::System(sys) = output else {
3328 panic!("Expected System message");
3329 };
3330 let compact = sys.as_compact_boundary().expect("compact_boundary");
3331 assert_eq!(
3332 compact.summary.as_deref(),
3333 Some("Summarized the earlier exploration.")
3334 );
3335 assert_eq!(compact.leaf_message_count, Some(42));
3336 assert_eq!(compact.duration_ms, Some(1234));
3337
3338 let json_alt = r#"{
3341 "type": "system",
3342 "subtype": "compact_boundary",
3343 "session_id": "s2",
3344 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
3345 "content": "alt-key summary",
3346 "message_count": 7
3347 }"#;
3348 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
3349 let ClaudeOutput::System(sys) = output else {
3350 panic!("Expected System message");
3351 };
3352 let compact = sys.as_compact_boundary().expect("compact_boundary");
3353 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
3354 assert_eq!(compact.leaf_message_count, Some(7));
3355 }
3356
3357 #[test]
3358 fn test_init_message_with_new_fields() {
3359 let json = r#"{
3360 "type": "system",
3361 "subtype": "init",
3362 "session_id": "test-session",
3363 "cwd": "/home/user",
3364 "model": "claude-opus-4-7",
3365 "tools": ["Bash"],
3366 "mcp_servers": [],
3367 "permissionMode": "default",
3368 "apiKeySource": "none",
3369 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
3370 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
3371 "fast_mode_state": "off",
3372 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
3373 "claude_code_version": "2.1.117"
3374 }"#;
3375
3376 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3377 if let ClaudeOutput::System(sys) = output {
3378 let init = sys.as_init().expect("Should parse as init");
3379 assert_eq!(
3380 init.uuid.as_deref(),
3381 Some("44841a0d-182d-493a-86b5-79800d3d9665")
3382 );
3383 assert!(init.memory_paths.is_some());
3384 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
3385 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
3386 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
3387 } else {
3388 panic!("Expected System message");
3389 }
3390 }
3391
3392 #[test]
3393 fn test_assistant_message_with_new_fields() {
3394 let json = r#"{
3395 "type": "assistant",
3396 "message": {
3397 "id": "msg_1",
3398 "type": "message",
3399 "role": "assistant",
3400 "model": "claude-opus-4-7",
3401 "content": [{"type": "text", "text": "Hello"}],
3402 "stop_reason": "end_turn",
3403 "stop_details": null,
3404 "context_management": null,
3405 "usage": {
3406 "input_tokens": 100,
3407 "output_tokens": 10,
3408 "cache_creation_input_tokens": 50,
3409 "cache_read_input_tokens": 0,
3410 "service_tier": "standard",
3411 "inference_geo": "not_available"
3412 }
3413 },
3414 "session_id": "abc",
3415 "uuid": "msg-uuid-123"
3416 }"#;
3417
3418 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3419 if let ClaudeOutput::Assistant(asst) = output {
3420 assert_eq!(asst.message.stop_details, None);
3421 assert_eq!(asst.message.context_management, None);
3422 let usage = asst.message.usage.unwrap();
3423 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
3424 } else {
3425 panic!("Expected Assistant message");
3426 }
3427 }
3428
3429 #[test]
3430 fn test_user_message_with_new_fields() {
3431 let json = r#"{
3432 "type": "user",
3433 "message": {
3434 "role": "user",
3435 "content": [{"type": "text", "text": "Hello"}]
3436 },
3437 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3438 "parent_tool_use_id": "toolu_123",
3439 "uuid": "user-msg-456"
3440 }"#;
3441
3442 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3443 if let ClaudeOutput::User(user) = output {
3444 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
3445 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
3446 } else {
3447 panic!("Expected User message");
3448 }
3449 }
3450
3451 #[test]
3457 fn test_user_message_preserves_tool_use_result_and_timestamp() {
3458 let json = r#"{
3459 "type":"user",
3460 "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"}]},
3461 "parent_tool_use_id":null,
3462 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3463 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
3464 "timestamp":"2026-05-12T23:12:04.121Z",
3465 "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"}}
3466 }"#;
3467
3468 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3469 let user = match output {
3470 ClaudeOutput::User(u) => u,
3471 other => panic!("Expected User message, got {:?}", other.message_type()),
3472 };
3473
3474 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
3475 let raw = user
3476 .tool_use_result
3477 .as_ref()
3478 .expect("tool_use_result must be captured");
3479 assert_eq!(raw["answers"]["Color"], "Blue");
3480 assert_eq!(raw["questions"][0]["header"], "Color");
3481
3482 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
3486 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
3487 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
3488 assert_eq!(
3489 reser["tool_use_result"]["questions"][0]["question"],
3490 "Which color do you prefer?"
3491 );
3492
3493 let typed: crate::AskUserQuestionInput = user
3496 .tool_use_result_as::<crate::AskUserQuestionInput>()
3497 .expect("tool_use_result present")
3498 .expect("AskUserQuestionInput parses");
3499 assert_eq!(typed.questions.len(), 1);
3500 assert_eq!(typed.questions[0].header, "Color");
3501 let answers = typed.answers.expect("answers populated");
3502 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3503 }
3504
3505 #[test]
3508 fn test_user_message_without_tool_use_result_omits_field() {
3509 let json = r#"{
3510 "type":"user",
3511 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3512 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3513 }"#;
3514
3515 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3516 let user = match output {
3517 ClaudeOutput::User(u) => u,
3518 _ => panic!("Expected User message"),
3519 };
3520 assert!(user.tool_use_result.is_none());
3521 assert!(user.timestamp.is_none());
3522
3523 let reser = serde_json::to_value(&user).unwrap();
3524 assert!(reser.get("tool_use_result").is_none());
3525 assert!(reser.get("timestamp").is_none());
3526 }
3527
3528 #[test]
3532 fn test_subagent_result_exposes_token_accounting() {
3533 let json = r#"{
3534 "type":"user",
3535 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3536 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3537 "tool_use_result":{
3538 "status":"completed",
3539 "prompt":"Count the .rs files.",
3540 "agentId":"ac4f0276e9d4b6232",
3541 "agentType":"Explore",
3542 "content":[{"type":"text","text":"21"}],
3543 "resolvedModel":"claude-haiku-4-5-20251001",
3544 "totalDurationMs":6869,
3545 "totalTokens":7834,
3546 "totalToolUseCount":1,
3547 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3548 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3549 }
3550 }"#;
3551
3552 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3553 let user = match output {
3554 ClaudeOutput::User(u) => u,
3555 _ => panic!("Expected User message"),
3556 };
3557
3558 let result = user.subagent_result().expect("subagent result parses");
3559 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3560 assert_eq!(
3561 result.resolved_model.as_deref(),
3562 Some("claude-haiku-4-5-20251001")
3563 );
3564 assert_eq!(result.total_tokens, Some(7834));
3565 assert_eq!(result.total_duration_ms, Some(6869));
3566 assert_eq!(result.total_tool_use_count, Some(1));
3567
3568 let usage = result.usage.expect("nested usage present");
3569 assert_eq!(usage.input_tokens, 6);
3570 assert_eq!(usage.cache_read_input_tokens, 7699);
3571
3572 let stats = result.tool_stats.expect("toolStats present");
3573 assert_eq!(stats.bash_count, 1);
3574 }
3575
3576 #[test]
3580 fn test_subagent_result_absent_for_non_task_result() {
3581 let json = r#"{
3582 "type":"user",
3583 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3584 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3585 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3586 }"#;
3587
3588 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3589 let user = match output {
3590 ClaudeOutput::User(u) => u,
3591 _ => panic!("Expected User message"),
3592 };
3593
3594 let result = user.subagent_result().expect("lenient parse");
3595 assert_eq!(result.total_tokens, None);
3596 assert_eq!(result.agent_type, None);
3597 }
3598
3599 #[test]
3600 fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3601 use serde_json::Value;
3602
3603 let raw: Value = serde_json::from_str(
3604 r#"{
3605 "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3606 "fast_mode_state":"off",
3607 "fast_mode_disabled_reason":"not_first_party",
3608 "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3609 }"#,
3610 )
3611 .unwrap();
3612 crate::io::assert_fully_wrapped(&raw);
3613
3614 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3615 let ClaudeOutput::System(sys) = output else {
3616 panic!("expected System");
3617 };
3618 let init = sys.as_init().expect("parses as init");
3619 assert_eq!(
3620 init.fast_mode_disabled_reason,
3621 Some(crate::FastModeDisabledReason::NotFirstParty)
3622 );
3623 let errs = init.mcp_server_errors.unwrap();
3624 assert_eq!(errs.len(), 1);
3625 assert_eq!(errs[0].name, "broken");
3626 assert_eq!(errs[0].error_type, "invalid_config");
3627 }
3628
3629 #[test]
3630 fn test_code_change_published_fully_wrapped() {
3631 use super::{KnownSystemEvent, SystemSubtype};
3632 use serde_json::Value;
3633
3634 let raw: Value = serde_json::from_str(
3635 r#"{
3636 "type":"system","subtype":"code_change_published",
3637 "provider":"github","url":"https://github.com/owner/repo/pull/42",
3638 "repo":"owner/repo","identifier":"42",
3639 "uuid":"u1","session_id":"s1"
3640 }"#,
3641 )
3642 .unwrap();
3643 crate::io::assert_fully_wrapped(&raw);
3644
3645 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3646 let ClaudeOutput::System(sys) = output else {
3647 panic!("expected System");
3648 };
3649 assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3650 let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3651 panic!("expected CodeChangePublished event");
3652 };
3653 assert_eq!(msg.provider, "github");
3654 assert_eq!(msg.repo, "owner/repo");
3655 assert_eq!(msg.identifier, "42");
3656
3657 assert!(sys.is_code_change_published());
3658 assert!(!sys.is_vcs_state_changed());
3659 let direct = sys.as_code_change_published().expect("direct accessor");
3660 assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3661 assert!(sys.as_vcs_state_changed().is_none());
3662 }
3663
3664 #[test]
3665 fn test_feedback_draft_queued_fully_wrapped() {
3666 use super::{KnownSystemEvent, SystemSubtype};
3667 use serde_json::Value;
3668
3669 let raw: Value = serde_json::from_str(
3670 r#"{
3671 "type":"system","subtype":"feedback_draft_queued",
3672 "draft_id":"draft-1","draft_type":"bug_report",
3673 "title":"Tool output was truncated",
3674 "details_preview":"The last command omitted its final lines",
3675 "uuid":"u1","session_id":"s1","future_field":"preserved"
3676 }"#,
3677 )
3678 .unwrap();
3679 crate::io::assert_fully_wrapped(&raw);
3680
3681 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3682 let ClaudeOutput::System(sys) = output else {
3683 panic!("expected System");
3684 };
3685 assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3686 assert!(sys.is_feedback_draft_queued());
3687 assert!(!sys.is_vcs_state_changed());
3688
3689 let direct = sys
3690 .as_feedback_draft_queued()
3691 .expect("direct typed accessor");
3692 assert_eq!(direct.draft_id, "draft-1");
3693 assert_eq!(direct.draft_type, "bug_report");
3694 assert_eq!(direct.extra["future_field"], "preserved");
3695
3696 let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3697 panic!("expected FeedbackDraftQueued event");
3698 };
3699 assert_eq!(known.title, "Tool output was truncated");
3700 assert_eq!(
3701 sys.typed_value().expect("typed value")["future_field"],
3702 "preserved"
3703 );
3704 }
3705
3706 #[test]
3707 fn test_cloud_session_delta_fully_wrapped() {
3708 use super::{KnownSystemEvent, SystemSubtype};
3709 use serde_json::Value;
3710
3711 let raw: Value = serde_json::from_str(
3712 r#"{
3713 "type":"system","subtype":"cloud_session_delta",
3714 "seq":3,"changed":["serving","connection"],
3715 "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
3716 "serving":{"state":"on"},"connection":{"state":"live"}},
3717 "uuid":"u1","session_id":"s1","future_field":"preserved"
3718 }"#,
3719 )
3720 .unwrap();
3721 crate::io::assert_fully_wrapped(&raw);
3722
3723 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3724 let ClaudeOutput::System(sys) = output else {
3725 panic!("expected System");
3726 };
3727 assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
3728 assert!(sys.is_cloud_session_delta());
3729 assert!(!sys.is_feedback_draft_queued());
3730
3731 let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
3732 assert_eq!(direct.seq, 3);
3733 assert_eq!(direct.changed, vec!["serving", "connection"]);
3734 assert_eq!(direct.cloud_session["id"], "session_abc");
3735 assert_eq!(direct.extra["future_field"], "preserved");
3736
3737 let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
3738 panic!("expected CloudSessionDelta event");
3739 };
3740 assert_eq!(known.session_id, "s1");
3741 assert_eq!(
3742 sys.typed_value().expect("typed value")["future_field"],
3743 "preserved"
3744 );
3745 }
3746
3747 #[test]
3748 fn test_vcs_state_changed_fully_wrapped() {
3749 use super::{KnownSystemEvent, VcsMutationKind};
3750 use serde_json::Value;
3751
3752 for kind in ["commit", "push", "merge", "rebase"] {
3753 let raw: Value = serde_json::from_str(&format!(
3754 r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3755 kind
3756 ))
3757 .unwrap();
3758 crate::io::assert_fully_wrapped(&raw);
3759
3760 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3761 let ClaudeOutput::System(sys) = output else {
3762 panic!("expected System");
3763 };
3764 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3765 panic!("expected VcsStateChanged event");
3766 };
3767 assert_eq!(msg.kind.as_str(), kind);
3768 assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3769 }
3770
3771 let raw: Value = serde_json::from_str(
3773 r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3774 )
3775 .unwrap();
3776 crate::io::assert_fully_wrapped(&raw);
3777 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3778 let ClaudeOutput::System(sys) = output else {
3779 panic!("expected System");
3780 };
3781 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3782 panic!("expected VcsStateChanged event");
3783 };
3784 assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3785
3786 assert!(sys.is_vcs_state_changed());
3787 let direct = sys.as_vcs_state_changed().expect("direct accessor");
3788 assert_eq!(direct.cwd, "/repo");
3789 assert!(sys.as_code_change_published().is_none());
3790 }
3791
3792 #[test]
3793 fn test_assistant_aborted_and_resume_flags_roundtrip() {
3794 let json = r#"{
3795 "type":"assistant",
3796 "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3797 "session_id":"s1",
3798 "aborted":true,
3799 "resumed_from_incomplete_thinking":true
3800 }"#;
3801 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3802 let ClaudeOutput::Assistant(msg) = &output else {
3803 panic!("expected Assistant");
3804 };
3805 assert_eq!(msg.aborted, Some(true));
3806 assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3807 let reserialized = serde_json::to_string(&output).unwrap();
3808 assert!(reserialized.contains("\"aborted\":true"));
3809 assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3810
3811 let json = r#"{
3813 "type":"assistant",
3814 "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3815 "session_id":"s2"
3816 }"#;
3817 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3818 let reserialized = serde_json::to_string(&output).unwrap();
3819 assert!(!reserialized.contains("aborted"));
3820 assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3821 }
3822
3823 #[test]
3824 fn test_user_tool_result_meta_roundtrip() {
3825 let json = r#"{
3826 "type":"user",
3827 "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3828 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3829 "tool_result_meta":[
3830 {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3831 {"id":"toolu_2","non_execution_kind":"permission-rule"}
3832 ]
3833 }"#;
3834 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3835 let ClaudeOutput::User(user) = &output else {
3836 panic!("expected User");
3837 };
3838 let meta = user.tool_result_meta.as_ref().unwrap();
3839 assert_eq!(meta.len(), 2);
3840 assert_eq!(meta[0].non_execution_kind, "user-rejected");
3841 assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3842 assert_eq!(meta[1].user_feedback, None);
3843
3844 let reserialized = serde_json::to_string(&output).unwrap();
3845 assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3846 assert!(!reserialized.contains("\"user_feedback\":null"));
3847 }
3848
3849 #[test]
3853 fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3854 use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3855 let with_scope = serde_json::json!({
3856 "trigger": "refusal",
3857 "direction": "retry",
3858 "scope": "local",
3859 "original_model": "claude-fable-5",
3860 "fallback_model": "claude-opus-5",
3861 "request_id": null,
3862 "content": "Refused; retried on fallback model.",
3863 "uuid": "u1",
3864 "session_id": "s1"
3865 });
3866 let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3867 assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3868 assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3869
3870 let mut without = with_scope.clone();
3873 without.as_object_mut().unwrap().remove("scope");
3874 let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3875 assert_eq!(msg.scope, None);
3876 assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3877
3878 assert_eq!(
3880 RefusalFallbackScope::from("workspace").as_str(),
3881 "workspace"
3882 );
3883 }
3884}