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")]
2681 pub user_message_uuid: Option<String>,
2682 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2688 pub user_message_uuids: Vec<String>,
2689 #[serde(skip_serializing_if = "Option::is_none")]
2692 pub subagent_type: Option<String>,
2693 #[serde(skip_serializing_if = "Option::is_none")]
2695 pub task_description: Option<String>,
2696 #[serde(skip_serializing_if = "Option::is_none")]
2697 pub error: Option<AssistantErrorKind>,
2698 #[serde(default, skip_serializing_if = "Option::is_none")]
2702 pub aborted: Option<bool>,
2703 #[serde(default, skip_serializing_if = "Option::is_none")]
2708 pub resumed_from_incomplete_thinking: Option<bool>,
2709 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2710 pub supersedes: Vec<String>,
2711 #[serde(skip_serializing_if = "Option::is_none")]
2712 pub timestamp: Option<String>,
2713 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2714 pub tool_use_meta: Vec<ToolUseMeta>,
2715 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2720 pub batch_tool_uses: Vec<BatchToolUse>,
2721 #[serde(default, skip_serializing_if = "Option::is_none")]
2727 pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
2728 #[serde(default, skip_serializing_if = "Option::is_none")]
2734 pub wire_ingest_context: Option<serde_json::Map<String, Value>>,
2735 #[serde(default, skip_serializing_if = "Option::is_none")]
2739 pub historical: Option<bool>,
2740 #[serde(default, skip_serializing_if = "Option::is_none")]
2746 pub local_command_source: Option<String>,
2747 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2756 pub narration_block_indexes: Vec<usize>,
2757 #[serde(default, skip_serializing_if = "Option::is_none")]
2761 pub context_usage: Option<ContextUsage>,
2762 #[serde(default, skip_serializing_if = "Option::is_none")]
2763 pub is_meta: Option<bool>,
2764 #[serde(default, skip_serializing_if = "Option::is_none")]
2765 pub is_virtual: Option<bool>,
2766 #[serde(default, skip_serializing_if = "Option::is_none")]
2767 pub is_api_error_message: Option<bool>,
2768 #[serde(skip_serializing_if = "Option::is_none")]
2769 pub api_error_status: Option<u16>,
2770 #[serde(skip_serializing_if = "Option::is_none")]
2771 pub api_error: Option<String>,
2772 #[serde(skip_serializing_if = "Option::is_none")]
2773 pub error_details: Option<String>,
2774 #[serde(skip_serializing_if = "Option::is_none")]
2775 pub advisor_model: Option<String>,
2776 #[serde(skip_serializing_if = "Option::is_none")]
2777 pub attribution_agent: Option<String>,
2778 #[serde(skip_serializing_if = "Option::is_none")]
2779 pub attribution_skill: Option<String>,
2780 #[serde(skip_serializing_if = "Option::is_none")]
2781 pub attribution_plugin: Option<String>,
2782 #[serde(skip_serializing_if = "Option::is_none")]
2783 pub attribution_mcp_server: Option<String>,
2784 #[serde(skip_serializing_if = "Option::is_none")]
2785 pub attribution_mcp_tool: Option<String>,
2786}
2787
2788#[derive(Debug, Clone, Serialize, Deserialize)]
2790pub struct AssistantMessageContent {
2791 pub id: String,
2792 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2794 pub message_type: Option<String>,
2795 pub role: MessageRole,
2796 pub model: String,
2797 pub content: Vec<ContentBlock>,
2798 #[serde(skip_serializing_if = "Option::is_none")]
2799 pub stop_reason: Option<StopReason>,
2800 #[serde(skip_serializing_if = "Option::is_none")]
2801 pub stop_sequence: Option<String>,
2802 #[serde(skip_serializing_if = "Option::is_none")]
2803 pub usage: Option<AssistantUsage>,
2804 #[serde(skip_serializing_if = "Option::is_none")]
2806 pub stop_details: Option<Value>,
2807 #[serde(skip_serializing_if = "Option::is_none")]
2809 pub context_management: Option<Value>,
2810}
2811
2812#[derive(Debug, Clone, Serialize, Deserialize)]
2814pub struct AssistantUsage {
2815 #[serde(default)]
2817 pub input_tokens: u32,
2818
2819 #[serde(default)]
2821 pub output_tokens: u32,
2822
2823 #[serde(default)]
2825 pub cache_creation_input_tokens: u32,
2826
2827 #[serde(default)]
2829 pub cache_read_input_tokens: u32,
2830
2831 #[serde(skip_serializing_if = "Option::is_none")]
2833 pub service_tier: Option<String>,
2834
2835 #[serde(skip_serializing_if = "Option::is_none")]
2837 pub cache_creation: Option<CacheCreationDetails>,
2838
2839 #[serde(skip_serializing_if = "Option::is_none")]
2841 pub inference_geo: Option<String>,
2842}
2843
2844#[derive(Debug, Clone, Serialize, Deserialize)]
2846pub struct CacheCreationDetails {
2847 #[serde(default)]
2849 pub ephemeral_1h_input_tokens: u32,
2850
2851 #[serde(default)]
2853 pub ephemeral_5m_input_tokens: u32,
2854}
2855
2856#[cfg(test)]
2857mod tests {
2858 use crate::io::ClaudeOutput;
2859
2860 #[test]
2861 fn test_subagent_usage_rollup_accumulates_task_results() {
2862 use super::SubagentUsageRollup;
2863
2864 let mut rollup = SubagentUsageRollup::default();
2865
2866 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}}"#;
2867 let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2868 assert!(rollup.observe(&output));
2869 assert_eq!(rollup.subagent_tokens, 10201);
2870 assert_eq!(rollup.agent_count, 1);
2871 assert_eq!(rollup.tool_uses, 3);
2872 assert_eq!(rollup.duration_ms, 1853);
2873
2874 assert!(!rollup.observe(&output));
2876 assert_eq!(rollup.agent_count, 1);
2877 assert_eq!(rollup.subagent_tokens, 10201);
2878
2879 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}}"#;
2881 let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2882 assert!(rollup.observe(&output));
2883 assert_eq!(rollup.agent_count, 2);
2884 assert_eq!(rollup.subagent_tokens, 10701);
2885 }
2886
2887 #[test]
2888 fn test_subagent_usage_rollup_ignores_non_task_results() {
2889 use super::SubagentUsageRollup;
2890
2891 let mut rollup = SubagentUsageRollup::default();
2892
2893 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}}"#;
2896 let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2897 assert!(!rollup.observe(&output));
2898
2899 let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2901 let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2902 assert!(!rollup.observe(&output));
2903
2904 let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2906 let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2907 assert!(!rollup.observe(&output));
2908
2909 assert_eq!(rollup, SubagentUsageRollup::default());
2910 }
2911
2912 #[test]
2913 fn test_subagent_usage_rollup_over_captured_session() {
2914 use super::SubagentUsageRollup;
2915
2916 let mut rollup = SubagentUsageRollup::default();
2917 let fixture =
2918 include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2919 for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2920 if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2921 rollup.observe(&output);
2922 }
2923 }
2924 assert_eq!(rollup.agent_count, 1);
2925 assert_eq!(rollup.subagent_tokens, 10201);
2926 }
2927
2928 #[test]
2929 fn test_system_message_init() {
2930 let json = r#"{
2931 "type": "system",
2932 "subtype": "init",
2933 "session_id": "test-session-123",
2934 "cwd": "/home/user/project",
2935 "model": "claude-sonnet-4",
2936 "tools": ["Bash", "Read", "Write"],
2937 "mcp_servers": [],
2938 "slash_commands": ["compact", "cost", "review"],
2939 "agents": ["Bash", "Explore", "Plan"],
2940 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2941 "skills": [],
2942 "claude_code_version": "2.1.15",
2943 "apiKeySource": "none",
2944 "output_style": "default",
2945 "permissionMode": "default"
2946 }"#;
2947
2948 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2949 if let ClaudeOutput::System(sys) = output {
2950 assert!(sys.is_init());
2951 assert!(!sys.is_status());
2952 assert!(!sys.is_compact_boundary());
2953
2954 let init = sys.as_init().expect("Should parse as init");
2955 assert_eq!(init.session_id, "test-session-123");
2956 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2957 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2958 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2959 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2960 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2961 assert_eq!(init.plugins.len(), 1);
2962 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2963 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2964 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2965 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2966 assert_eq!(
2967 init.permission_mode,
2968 Some(super::InitPermissionMode::Default)
2969 );
2970 } else {
2971 panic!("Expected System message");
2972 }
2973 }
2974
2975 #[test]
2976 fn test_system_message_init_from_real_capture() {
2977 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2978 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2979 if let ClaudeOutput::System(sys) = output {
2980 let init = sys.as_init().expect("Should parse real init capture");
2981 assert_eq!(init.slash_commands.len(), 8);
2982 assert!(init.slash_commands.contains(&"compact".to_string()));
2983 assert!(init.slash_commands.contains(&"review".to_string()));
2984 assert_eq!(init.agents.len(), 5);
2985 assert!(init.agents.contains(&"Bash".to_string()));
2986 assert!(init.agents.contains(&"Explore".to_string()));
2987 assert_eq!(init.plugins.len(), 1);
2988 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2989 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2990 } else {
2991 panic!("Expected System message");
2992 }
2993 }
2994
2995 #[test]
2996 fn test_system_message_status() {
2997 let json = r#"{
2998 "type": "system",
2999 "subtype": "status",
3000 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3001 "status": "compacting",
3002 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
3003 }"#;
3004
3005 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3006 if let ClaudeOutput::System(sys) = output {
3007 assert!(sys.is_status());
3008 assert!(!sys.is_init());
3009
3010 let status = sys.as_status().expect("Should parse as status");
3011 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3012 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
3013 assert_eq!(
3014 status.uuid,
3015 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
3016 );
3017 } else {
3018 panic!("Expected System message");
3019 }
3020 }
3021
3022 #[test]
3023 fn test_system_message_status_null() {
3024 let json = r#"{
3025 "type": "system",
3026 "subtype": "status",
3027 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3028 "status": null,
3029 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
3030 }"#;
3031
3032 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3033 if let ClaudeOutput::System(sys) = output {
3034 let status = sys.as_status().expect("Should parse as status");
3035 assert_eq!(status.status, None);
3036 } else {
3037 panic!("Expected System message");
3038 }
3039 }
3040
3041 #[test]
3042 fn test_system_message_task_started() {
3043 let json = r#"{
3044 "type": "system",
3045 "subtype": "task_started",
3046 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3047 "task_id": "b6daf3f",
3048 "task_type": "local_bash",
3049 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
3050 "description": "Wait for CI on PR #12",
3051 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
3052 }"#;
3053
3054 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3055 if let ClaudeOutput::System(sys) = output {
3056 assert!(sys.is_task_started());
3057 assert!(!sys.is_task_progress());
3058 assert!(!sys.is_task_notification());
3059
3060 let task = sys.as_task_started().expect("Should parse as task_started");
3061 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
3062 assert_eq!(task.task_id, "b6daf3f");
3063 assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
3064 assert_eq!(
3065 task.tool_use_id.as_deref(),
3066 Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
3067 );
3068 assert_eq!(task.description, "Wait for CI on PR #12");
3069 } else {
3070 panic!("Expected System message");
3071 }
3072 }
3073
3074 #[test]
3075 fn test_system_message_task_started_agent() {
3076 let json = r#"{
3077 "type": "system",
3078 "subtype": "task_started",
3079 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3080 "task_id": "a4a7e0906e5fc64cc",
3081 "task_type": "local_agent",
3082 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3083 "description": "Explore Scene/ArrayScene duplication",
3084 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3085 }"#;
3086
3087 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3088 if let ClaudeOutput::System(sys) = output {
3089 let task = sys.as_task_started().expect("Should parse as task_started");
3090 assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
3091 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
3092 } else {
3093 panic!("Expected System message");
3094 }
3095 }
3096
3097 #[test]
3098 fn test_system_message_task_progress() {
3099 let json = r#"{
3100 "type": "system",
3101 "subtype": "task_progress",
3102 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3103 "task_id": "a4a7e0906e5fc64cc",
3104 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3105 "description": "Reading src/jplephem/chebyshev.rs",
3106 "last_tool_name": "Read",
3107 "usage": {
3108 "duration_ms": 13996,
3109 "tool_uses": 9,
3110 "total_tokens": 38779
3111 },
3112 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3113 }"#;
3114
3115 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3116 if let ClaudeOutput::System(sys) = output {
3117 assert!(sys.is_task_progress());
3118 assert!(!sys.is_task_started());
3119
3120 let progress = sys
3121 .as_task_progress()
3122 .expect("Should parse as task_progress");
3123 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
3124 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
3125 assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
3126 assert_eq!(progress.usage.duration_ms, 13996);
3127 assert_eq!(progress.usage.tool_uses, 9);
3128 assert_eq!(progress.usage.total_tokens, 38779);
3129 } else {
3130 panic!("Expected System message");
3131 }
3132 }
3133
3134 #[test]
3135 fn test_system_message_task_notification_completed() {
3136 let json = r#"{
3137 "type": "system",
3138 "subtype": "task_notification",
3139 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3140 "task_id": "a0ba761e9dc9c316f",
3141 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
3142 "status": "completed",
3143 "summary": "Agent \"Write Hipparcos data source doc\" completed",
3144 "output_file": "",
3145 "usage": {
3146 "duration_ms": 172300,
3147 "tool_uses": 11,
3148 "total_tokens": 42005
3149 },
3150 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
3151 }"#;
3152
3153 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3154 if let ClaudeOutput::System(sys) = output {
3155 assert!(sys.is_task_notification());
3156
3157 let notif = sys
3158 .as_task_notification()
3159 .expect("Should parse as task_notification");
3160 assert_eq!(notif.status, super::TaskStatus::Completed);
3161 assert_eq!(
3162 notif.summary,
3163 "Agent \"Write Hipparcos data source doc\" completed"
3164 );
3165 assert_eq!(notif.output_file, Some("".to_string()));
3166 assert_eq!(
3167 notif.tool_use_id,
3168 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
3169 );
3170 let usage = notif.usage.expect("Should have usage");
3171 assert_eq!(usage.duration_ms, 172300);
3172 assert_eq!(usage.tool_uses, 11);
3173 assert_eq!(usage.total_tokens, 42005);
3174 } else {
3175 panic!("Expected System message");
3176 }
3177 }
3178
3179 #[test]
3180 fn test_system_message_task_notification_failed_no_usage() {
3181 let json = r#"{
3182 "type": "system",
3183 "subtype": "task_notification",
3184 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
3185 "task_id": "b98f6a3",
3186 "status": "failed",
3187 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
3188 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
3189 }"#;
3190
3191 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3192 if let ClaudeOutput::System(sys) = output {
3193 let notif = sys
3194 .as_task_notification()
3195 .expect("Should parse as task_notification");
3196 assert_eq!(notif.status, super::TaskStatus::Failed);
3197 assert!(notif.tool_use_id.is_none());
3198 assert!(notif.usage.is_none());
3199 assert_eq!(
3200 notif.output_file,
3201 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
3202 );
3203 } else {
3204 panic!("Expected System message");
3205 }
3206 }
3207
3208 #[test]
3214 fn test_task_messages_roundtrip_through_value() {
3215 let cases = [
3216 r#"{"type":"system","subtype":"task_started","session_id":"s1",
3217 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
3218 "description":"Sleep 3s","uuid":"u1"}"#,
3219 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
3220 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
3221 "last_tool_name":"Bash",
3222 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3223 "uuid":"u2"}"#,
3224 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
3225 "task_id":"t1","tool_use_id":"tu1","status":"completed",
3226 "summary":"done","output_file":"",
3227 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3228 "uuid":"u3"}"#,
3229 ];
3230
3231 for json in cases {
3232 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3233 let value = serde_json::to_value(&output).unwrap();
3234 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
3235
3236 let ClaudeOutput::System(sys) = reparsed else {
3237 panic!("Expected System variant after round-trip");
3238 };
3239
3240 match sys.subtype {
3241 super::SystemSubtype::TaskStarted => {
3242 assert!(
3243 sys.as_task_started().is_some(),
3244 "as_task_started failed after round-trip"
3245 );
3246 }
3247 super::SystemSubtype::TaskProgress => {
3248 assert!(
3249 sys.as_task_progress().is_some(),
3250 "as_task_progress failed after round-trip"
3251 );
3252 }
3253 super::SystemSubtype::TaskNotification => {
3254 assert!(
3255 sys.as_task_notification().is_some(),
3256 "as_task_notification failed after round-trip"
3257 );
3258 }
3259 other => panic!("unexpected subtype after round-trip: {other:?}"),
3260 }
3261 }
3262 }
3263
3264 #[test]
3265 fn test_system_message_compact_boundary() {
3266 let json = r#"{
3267 "type": "system",
3268 "subtype": "compact_boundary",
3269 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3270 "compact_metadata": {
3271 "pre_tokens": 155285,
3272 "trigger": "auto"
3273 },
3274 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
3275 }"#;
3276
3277 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3278 if let ClaudeOutput::System(sys) = output {
3279 assert!(sys.is_compact_boundary());
3280 assert!(!sys.is_init());
3281 assert!(!sys.is_status());
3282
3283 let compact = sys
3284 .as_compact_boundary()
3285 .expect("Should parse as compact_boundary");
3286 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3287 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
3288 assert_eq!(
3289 compact.compact_metadata.trigger,
3290 super::CompactionTrigger::Auto
3291 );
3292 assert!(compact.summary.is_none());
3294 assert!(compact.leaf_message_count.is_none());
3295 assert!(compact.duration_ms.is_none());
3296 } else {
3297 panic!("Expected System message");
3298 }
3299 }
3300
3301 #[test]
3302 fn test_compact_boundary_with_summary_stats() {
3303 let json = r#"{
3305 "type": "system",
3306 "subtype": "compact_boundary",
3307 "session_id": "s1",
3308 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
3309 "summary": "Summarized the earlier exploration.",
3310 "leaf_message_count": 42,
3311 "duration_ms": 1234,
3312 "uuid": "u1"
3313 }"#;
3314 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3315 let ClaudeOutput::System(sys) = output else {
3316 panic!("Expected System message");
3317 };
3318 let compact = sys.as_compact_boundary().expect("compact_boundary");
3319 assert_eq!(
3320 compact.summary.as_deref(),
3321 Some("Summarized the earlier exploration.")
3322 );
3323 assert_eq!(compact.leaf_message_count, Some(42));
3324 assert_eq!(compact.duration_ms, Some(1234));
3325
3326 let json_alt = r#"{
3329 "type": "system",
3330 "subtype": "compact_boundary",
3331 "session_id": "s2",
3332 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
3333 "content": "alt-key summary",
3334 "message_count": 7
3335 }"#;
3336 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
3337 let ClaudeOutput::System(sys) = output else {
3338 panic!("Expected System message");
3339 };
3340 let compact = sys.as_compact_boundary().expect("compact_boundary");
3341 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
3342 assert_eq!(compact.leaf_message_count, Some(7));
3343 }
3344
3345 #[test]
3346 fn test_init_message_with_new_fields() {
3347 let json = r#"{
3348 "type": "system",
3349 "subtype": "init",
3350 "session_id": "test-session",
3351 "cwd": "/home/user",
3352 "model": "claude-opus-4-7",
3353 "tools": ["Bash"],
3354 "mcp_servers": [],
3355 "permissionMode": "default",
3356 "apiKeySource": "none",
3357 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
3358 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
3359 "fast_mode_state": "off",
3360 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
3361 "claude_code_version": "2.1.117"
3362 }"#;
3363
3364 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3365 if let ClaudeOutput::System(sys) = output {
3366 let init = sys.as_init().expect("Should parse as init");
3367 assert_eq!(
3368 init.uuid.as_deref(),
3369 Some("44841a0d-182d-493a-86b5-79800d3d9665")
3370 );
3371 assert!(init.memory_paths.is_some());
3372 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
3373 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
3374 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
3375 } else {
3376 panic!("Expected System message");
3377 }
3378 }
3379
3380 #[test]
3381 fn test_assistant_message_with_new_fields() {
3382 let json = r#"{
3383 "type": "assistant",
3384 "message": {
3385 "id": "msg_1",
3386 "type": "message",
3387 "role": "assistant",
3388 "model": "claude-opus-4-7",
3389 "content": [{"type": "text", "text": "Hello"}],
3390 "stop_reason": "end_turn",
3391 "stop_details": null,
3392 "context_management": null,
3393 "usage": {
3394 "input_tokens": 100,
3395 "output_tokens": 10,
3396 "cache_creation_input_tokens": 50,
3397 "cache_read_input_tokens": 0,
3398 "service_tier": "standard",
3399 "inference_geo": "not_available"
3400 }
3401 },
3402 "session_id": "abc",
3403 "uuid": "msg-uuid-123"
3404 }"#;
3405
3406 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3407 if let ClaudeOutput::Assistant(asst) = output {
3408 assert_eq!(asst.message.stop_details, None);
3409 assert_eq!(asst.message.context_management, None);
3410 let usage = asst.message.usage.unwrap();
3411 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
3412 } else {
3413 panic!("Expected Assistant message");
3414 }
3415 }
3416
3417 #[test]
3418 fn test_user_message_with_new_fields() {
3419 let json = r#"{
3420 "type": "user",
3421 "message": {
3422 "role": "user",
3423 "content": [{"type": "text", "text": "Hello"}]
3424 },
3425 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3426 "parent_tool_use_id": "toolu_123",
3427 "uuid": "user-msg-456"
3428 }"#;
3429
3430 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3431 if let ClaudeOutput::User(user) = output {
3432 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
3433 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
3434 } else {
3435 panic!("Expected User message");
3436 }
3437 }
3438
3439 #[test]
3445 fn test_user_message_preserves_tool_use_result_and_timestamp() {
3446 let json = r#"{
3447 "type":"user",
3448 "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"}]},
3449 "parent_tool_use_id":null,
3450 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3451 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
3452 "timestamp":"2026-05-12T23:12:04.121Z",
3453 "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"}}
3454 }"#;
3455
3456 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3457 let user = match output {
3458 ClaudeOutput::User(u) => u,
3459 other => panic!("Expected User message, got {:?}", other.message_type()),
3460 };
3461
3462 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
3463 let raw = user
3464 .tool_use_result
3465 .as_ref()
3466 .expect("tool_use_result must be captured");
3467 assert_eq!(raw["answers"]["Color"], "Blue");
3468 assert_eq!(raw["questions"][0]["header"], "Color");
3469
3470 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
3474 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
3475 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
3476 assert_eq!(
3477 reser["tool_use_result"]["questions"][0]["question"],
3478 "Which color do you prefer?"
3479 );
3480
3481 let typed: crate::AskUserQuestionInput = user
3484 .tool_use_result_as::<crate::AskUserQuestionInput>()
3485 .expect("tool_use_result present")
3486 .expect("AskUserQuestionInput parses");
3487 assert_eq!(typed.questions.len(), 1);
3488 assert_eq!(typed.questions[0].header, "Color");
3489 let answers = typed.answers.expect("answers populated");
3490 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3491 }
3492
3493 #[test]
3496 fn test_user_message_without_tool_use_result_omits_field() {
3497 let json = r#"{
3498 "type":"user",
3499 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3500 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3501 }"#;
3502
3503 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3504 let user = match output {
3505 ClaudeOutput::User(u) => u,
3506 _ => panic!("Expected User message"),
3507 };
3508 assert!(user.tool_use_result.is_none());
3509 assert!(user.timestamp.is_none());
3510
3511 let reser = serde_json::to_value(&user).unwrap();
3512 assert!(reser.get("tool_use_result").is_none());
3513 assert!(reser.get("timestamp").is_none());
3514 }
3515
3516 #[test]
3520 fn test_subagent_result_exposes_token_accounting() {
3521 let json = r#"{
3522 "type":"user",
3523 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3524 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3525 "tool_use_result":{
3526 "status":"completed",
3527 "prompt":"Count the .rs files.",
3528 "agentId":"ac4f0276e9d4b6232",
3529 "agentType":"Explore",
3530 "content":[{"type":"text","text":"21"}],
3531 "resolvedModel":"claude-haiku-4-5-20251001",
3532 "totalDurationMs":6869,
3533 "totalTokens":7834,
3534 "totalToolUseCount":1,
3535 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3536 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3537 }
3538 }"#;
3539
3540 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3541 let user = match output {
3542 ClaudeOutput::User(u) => u,
3543 _ => panic!("Expected User message"),
3544 };
3545
3546 let result = user.subagent_result().expect("subagent result parses");
3547 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3548 assert_eq!(
3549 result.resolved_model.as_deref(),
3550 Some("claude-haiku-4-5-20251001")
3551 );
3552 assert_eq!(result.total_tokens, Some(7834));
3553 assert_eq!(result.total_duration_ms, Some(6869));
3554 assert_eq!(result.total_tool_use_count, Some(1));
3555
3556 let usage = result.usage.expect("nested usage present");
3557 assert_eq!(usage.input_tokens, 6);
3558 assert_eq!(usage.cache_read_input_tokens, 7699);
3559
3560 let stats = result.tool_stats.expect("toolStats present");
3561 assert_eq!(stats.bash_count, 1);
3562 }
3563
3564 #[test]
3568 fn test_subagent_result_absent_for_non_task_result() {
3569 let json = r#"{
3570 "type":"user",
3571 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3572 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3573 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3574 }"#;
3575
3576 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3577 let user = match output {
3578 ClaudeOutput::User(u) => u,
3579 _ => panic!("Expected User message"),
3580 };
3581
3582 let result = user.subagent_result().expect("lenient parse");
3583 assert_eq!(result.total_tokens, None);
3584 assert_eq!(result.agent_type, None);
3585 }
3586
3587 #[test]
3588 fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3589 use serde_json::Value;
3590
3591 let raw: Value = serde_json::from_str(
3592 r#"{
3593 "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3594 "fast_mode_state":"off",
3595 "fast_mode_disabled_reason":"not_first_party",
3596 "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3597 }"#,
3598 )
3599 .unwrap();
3600 crate::io::assert_fully_wrapped(&raw);
3601
3602 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3603 let ClaudeOutput::System(sys) = output else {
3604 panic!("expected System");
3605 };
3606 let init = sys.as_init().expect("parses as init");
3607 assert_eq!(
3608 init.fast_mode_disabled_reason,
3609 Some(crate::FastModeDisabledReason::NotFirstParty)
3610 );
3611 let errs = init.mcp_server_errors.unwrap();
3612 assert_eq!(errs.len(), 1);
3613 assert_eq!(errs[0].name, "broken");
3614 assert_eq!(errs[0].error_type, "invalid_config");
3615 }
3616
3617 #[test]
3618 fn test_code_change_published_fully_wrapped() {
3619 use super::{KnownSystemEvent, SystemSubtype};
3620 use serde_json::Value;
3621
3622 let raw: Value = serde_json::from_str(
3623 r#"{
3624 "type":"system","subtype":"code_change_published",
3625 "provider":"github","url":"https://github.com/owner/repo/pull/42",
3626 "repo":"owner/repo","identifier":"42",
3627 "uuid":"u1","session_id":"s1"
3628 }"#,
3629 )
3630 .unwrap();
3631 crate::io::assert_fully_wrapped(&raw);
3632
3633 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3634 let ClaudeOutput::System(sys) = output else {
3635 panic!("expected System");
3636 };
3637 assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3638 let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3639 panic!("expected CodeChangePublished event");
3640 };
3641 assert_eq!(msg.provider, "github");
3642 assert_eq!(msg.repo, "owner/repo");
3643 assert_eq!(msg.identifier, "42");
3644
3645 assert!(sys.is_code_change_published());
3646 assert!(!sys.is_vcs_state_changed());
3647 let direct = sys.as_code_change_published().expect("direct accessor");
3648 assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3649 assert!(sys.as_vcs_state_changed().is_none());
3650 }
3651
3652 #[test]
3653 fn test_feedback_draft_queued_fully_wrapped() {
3654 use super::{KnownSystemEvent, SystemSubtype};
3655 use serde_json::Value;
3656
3657 let raw: Value = serde_json::from_str(
3658 r#"{
3659 "type":"system","subtype":"feedback_draft_queued",
3660 "draft_id":"draft-1","draft_type":"bug_report",
3661 "title":"Tool output was truncated",
3662 "details_preview":"The last command omitted its final lines",
3663 "uuid":"u1","session_id":"s1","future_field":"preserved"
3664 }"#,
3665 )
3666 .unwrap();
3667 crate::io::assert_fully_wrapped(&raw);
3668
3669 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3670 let ClaudeOutput::System(sys) = output else {
3671 panic!("expected System");
3672 };
3673 assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3674 assert!(sys.is_feedback_draft_queued());
3675 assert!(!sys.is_vcs_state_changed());
3676
3677 let direct = sys
3678 .as_feedback_draft_queued()
3679 .expect("direct typed accessor");
3680 assert_eq!(direct.draft_id, "draft-1");
3681 assert_eq!(direct.draft_type, "bug_report");
3682 assert_eq!(direct.extra["future_field"], "preserved");
3683
3684 let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3685 panic!("expected FeedbackDraftQueued event");
3686 };
3687 assert_eq!(known.title, "Tool output was truncated");
3688 assert_eq!(
3689 sys.typed_value().expect("typed value")["future_field"],
3690 "preserved"
3691 );
3692 }
3693
3694 #[test]
3695 fn test_cloud_session_delta_fully_wrapped() {
3696 use super::{KnownSystemEvent, SystemSubtype};
3697 use serde_json::Value;
3698
3699 let raw: Value = serde_json::from_str(
3700 r#"{
3701 "type":"system","subtype":"cloud_session_delta",
3702 "seq":3,"changed":["serving","connection"],
3703 "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
3704 "serving":{"state":"on"},"connection":{"state":"live"}},
3705 "uuid":"u1","session_id":"s1","future_field":"preserved"
3706 }"#,
3707 )
3708 .unwrap();
3709 crate::io::assert_fully_wrapped(&raw);
3710
3711 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3712 let ClaudeOutput::System(sys) = output else {
3713 panic!("expected System");
3714 };
3715 assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
3716 assert!(sys.is_cloud_session_delta());
3717 assert!(!sys.is_feedback_draft_queued());
3718
3719 let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
3720 assert_eq!(direct.seq, 3);
3721 assert_eq!(direct.changed, vec!["serving", "connection"]);
3722 assert_eq!(direct.cloud_session["id"], "session_abc");
3723 assert_eq!(direct.extra["future_field"], "preserved");
3724
3725 let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
3726 panic!("expected CloudSessionDelta event");
3727 };
3728 assert_eq!(known.session_id, "s1");
3729 assert_eq!(
3730 sys.typed_value().expect("typed value")["future_field"],
3731 "preserved"
3732 );
3733 }
3734
3735 #[test]
3736 fn test_vcs_state_changed_fully_wrapped() {
3737 use super::{KnownSystemEvent, VcsMutationKind};
3738 use serde_json::Value;
3739
3740 for kind in ["commit", "push", "merge", "rebase"] {
3741 let raw: Value = serde_json::from_str(&format!(
3742 r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3743 kind
3744 ))
3745 .unwrap();
3746 crate::io::assert_fully_wrapped(&raw);
3747
3748 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3749 let ClaudeOutput::System(sys) = output else {
3750 panic!("expected System");
3751 };
3752 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3753 panic!("expected VcsStateChanged event");
3754 };
3755 assert_eq!(msg.kind.as_str(), kind);
3756 assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3757 }
3758
3759 let raw: Value = serde_json::from_str(
3761 r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3762 )
3763 .unwrap();
3764 crate::io::assert_fully_wrapped(&raw);
3765 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3766 let ClaudeOutput::System(sys) = output else {
3767 panic!("expected System");
3768 };
3769 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3770 panic!("expected VcsStateChanged event");
3771 };
3772 assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3773
3774 assert!(sys.is_vcs_state_changed());
3775 let direct = sys.as_vcs_state_changed().expect("direct accessor");
3776 assert_eq!(direct.cwd, "/repo");
3777 assert!(sys.as_code_change_published().is_none());
3778 }
3779
3780 #[test]
3781 fn test_assistant_aborted_and_resume_flags_roundtrip() {
3782 let json = r#"{
3783 "type":"assistant",
3784 "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3785 "session_id":"s1",
3786 "aborted":true,
3787 "resumed_from_incomplete_thinking":true
3788 }"#;
3789 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3790 let ClaudeOutput::Assistant(msg) = &output else {
3791 panic!("expected Assistant");
3792 };
3793 assert_eq!(msg.aborted, Some(true));
3794 assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3795 let reserialized = serde_json::to_string(&output).unwrap();
3796 assert!(reserialized.contains("\"aborted\":true"));
3797 assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3798
3799 let json = r#"{
3801 "type":"assistant",
3802 "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3803 "session_id":"s2"
3804 }"#;
3805 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3806 let reserialized = serde_json::to_string(&output).unwrap();
3807 assert!(!reserialized.contains("aborted"));
3808 assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3809 }
3810
3811 #[test]
3812 fn test_user_tool_result_meta_roundtrip() {
3813 let json = r#"{
3814 "type":"user",
3815 "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3816 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3817 "tool_result_meta":[
3818 {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3819 {"id":"toolu_2","non_execution_kind":"permission-rule"}
3820 ]
3821 }"#;
3822 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3823 let ClaudeOutput::User(user) = &output else {
3824 panic!("expected User");
3825 };
3826 let meta = user.tool_result_meta.as_ref().unwrap();
3827 assert_eq!(meta.len(), 2);
3828 assert_eq!(meta[0].non_execution_kind, "user-rejected");
3829 assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3830 assert_eq!(meta[1].user_feedback, None);
3831
3832 let reserialized = serde_json::to_string(&output).unwrap();
3833 assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3834 assert!(!reserialized.contains("\"user_feedback\":null"));
3835 }
3836
3837 #[test]
3841 fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3842 use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3843 let with_scope = serde_json::json!({
3844 "trigger": "refusal",
3845 "direction": "retry",
3846 "scope": "local",
3847 "original_model": "claude-fable-5",
3848 "fallback_model": "claude-opus-5",
3849 "request_id": null,
3850 "content": "Refused; retried on fallback model.",
3851 "uuid": "u1",
3852 "session_id": "s1"
3853 });
3854 let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3855 assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3856 assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3857
3858 let mut without = with_scope.clone();
3861 without.as_object_mut().unwrap().remove("scope");
3862 let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3863 assert_eq!(msg.scope, None);
3864 assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3865
3866 assert_eq!(
3868 RefusalFallbackScope::from("workspace").as_str(),
3869 "workspace"
3870 );
3871 }
3872}