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 Unknown(String),
50}
51
52impl SystemSubtype {
53 pub fn as_str(&self) -> &str {
54 match self {
55 Self::Init => "init",
56 Self::Status => "status",
57 Self::CompactBoundary => "compact_boundary",
58 Self::ThinkingTokens => "thinking_tokens",
59 Self::TaskStarted => "task_started",
60 Self::TaskProgress => "task_progress",
61 Self::TaskUpdated => "task_updated",
62 Self::TaskNotification => "task_notification",
63 Self::ApiRetry => "api_retry",
64 Self::ControlRequestProgress => "control_request_progress",
65 Self::ModelRefusalFallback => "model_refusal_fallback",
66 Self::ModelRefusalNoFallback => "model_refusal_no_fallback",
67 Self::LocalCommandOutput => "local_command_output",
68 Self::HookStarted => "hook_started",
69 Self::HookProgress => "hook_progress",
70 Self::HookResponse => "hook_response",
71 Self::PluginInstall => "plugin_install",
72 Self::BackgroundTasksChanged => "background_tasks_changed",
73 Self::SessionStateChanged => "session_state_changed",
74 Self::WorkerShuttingDown => "worker_shutting_down",
75 Self::CommandsChanged => "commands_changed",
76 Self::Notification => "notification",
77 Self::FilesPersisted => "files_persisted",
78 Self::MemoryRecall => "memory_recall",
79 Self::ElicitationComplete => "elicitation_complete",
80 Self::PermissionDenied => "permission_denied",
81 Self::MirrorError => "mirror_error",
82 Self::Informational => "informational",
83 Self::CodeChangePublished => "code_change_published",
84 Self::VcsStateChanged => "vcs_state_changed",
85 Self::FeedbackDraftQueued => "feedback_draft_queued",
86 Self::CloudSessionDelta => "cloud_session_delta",
87 Self::Unknown(s) => s.as_str(),
88 }
89 }
90}
91
92impl fmt::Display for SystemSubtype {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.write_str(self.as_str())
95 }
96}
97
98impl From<&str> for SystemSubtype {
99 fn from(s: &str) -> Self {
100 match s {
101 "init" => Self::Init,
102 "status" => Self::Status,
103 "compact_boundary" => Self::CompactBoundary,
104 "thinking_tokens" => Self::ThinkingTokens,
105 "task_started" => Self::TaskStarted,
106 "task_progress" => Self::TaskProgress,
107 "task_updated" => Self::TaskUpdated,
108 "task_notification" => Self::TaskNotification,
109 "api_retry" => Self::ApiRetry,
110 "control_request_progress" => Self::ControlRequestProgress,
111 "model_refusal_fallback" => Self::ModelRefusalFallback,
112 "model_refusal_no_fallback" => Self::ModelRefusalNoFallback,
113 "local_command_output" => Self::LocalCommandOutput,
114 "hook_started" => Self::HookStarted,
115 "hook_progress" => Self::HookProgress,
116 "hook_response" => Self::HookResponse,
117 "plugin_install" => Self::PluginInstall,
118 "background_tasks_changed" => Self::BackgroundTasksChanged,
119 "session_state_changed" => Self::SessionStateChanged,
120 "worker_shutting_down" => Self::WorkerShuttingDown,
121 "commands_changed" => Self::CommandsChanged,
122 "notification" => Self::Notification,
123 "files_persisted" => Self::FilesPersisted,
124 "memory_recall" => Self::MemoryRecall,
125 "elicitation_complete" => Self::ElicitationComplete,
126 "permission_denied" => Self::PermissionDenied,
127 "mirror_error" => Self::MirrorError,
128 "informational" => Self::Informational,
129 "code_change_published" => Self::CodeChangePublished,
130 "vcs_state_changed" => Self::VcsStateChanged,
131 "feedback_draft_queued" => Self::FeedbackDraftQueued,
132 "cloud_session_delta" => Self::CloudSessionDelta,
133 other => Self::Unknown(other.to_string()),
134 }
135 }
136}
137
138impl Serialize for SystemSubtype {
139 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
140 serializer.serialize_str(self.as_str())
141 }
142}
143
144impl<'de> Deserialize<'de> for SystemSubtype {
145 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
146 let s = String::deserialize(deserializer)?;
147 Ok(Self::from(s.as_str()))
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Hash)]
156pub enum MessageRole {
157 User,
158 Assistant,
159 Unknown(String),
161}
162
163impl MessageRole {
164 pub fn as_str(&self) -> &str {
165 match self {
166 Self::User => "user",
167 Self::Assistant => "assistant",
168 Self::Unknown(s) => s.as_str(),
169 }
170 }
171}
172
173impl fmt::Display for MessageRole {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 f.write_str(self.as_str())
176 }
177}
178
179impl From<&str> for MessageRole {
180 fn from(s: &str) -> Self {
181 match s {
182 "user" => Self::User,
183 "assistant" => Self::Assistant,
184 other => Self::Unknown(other.to_string()),
185 }
186 }
187}
188
189impl Serialize for MessageRole {
190 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
191 serializer.serialize_str(self.as_str())
192 }
193}
194
195impl<'de> Deserialize<'de> for MessageRole {
196 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
197 let s = String::deserialize(deserializer)?;
198 Ok(Self::from(s.as_str()))
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Hash)]
204pub enum CompactionTrigger {
205 Auto,
207 Manual,
209 Unknown(String),
211}
212
213impl CompactionTrigger {
214 pub fn as_str(&self) -> &str {
215 match self {
216 Self::Auto => "auto",
217 Self::Manual => "manual",
218 Self::Unknown(s) => s.as_str(),
219 }
220 }
221}
222
223impl fmt::Display for CompactionTrigger {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str(self.as_str())
226 }
227}
228
229impl From<&str> for CompactionTrigger {
230 fn from(s: &str) -> Self {
231 match s {
232 "auto" => Self::Auto,
233 "manual" => Self::Manual,
234 other => Self::Unknown(other.to_string()),
235 }
236 }
237}
238
239impl Serialize for CompactionTrigger {
240 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
241 serializer.serialize_str(self.as_str())
242 }
243}
244
245impl<'de> Deserialize<'de> for CompactionTrigger {
246 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
247 let s = String::deserialize(deserializer)?;
248 Ok(Self::from(s.as_str()))
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Hash)]
254pub enum StopReason {
255 EndTurn,
257 MaxTokens,
259 ToolUse,
261 Unknown(String),
263}
264
265impl StopReason {
266 pub fn as_str(&self) -> &str {
267 match self {
268 Self::EndTurn => "end_turn",
269 Self::MaxTokens => "max_tokens",
270 Self::ToolUse => "tool_use",
271 Self::Unknown(s) => s.as_str(),
272 }
273 }
274}
275
276impl fmt::Display for StopReason {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 f.write_str(self.as_str())
279 }
280}
281
282impl From<&str> for StopReason {
283 fn from(s: &str) -> Self {
284 match s {
285 "end_turn" => Self::EndTurn,
286 "max_tokens" => Self::MaxTokens,
287 "tool_use" => Self::ToolUse,
288 other => Self::Unknown(other.to_string()),
289 }
290 }
291}
292
293impl Serialize for StopReason {
294 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
295 serializer.serialize_str(self.as_str())
296 }
297}
298
299impl<'de> Deserialize<'de> for StopReason {
300 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
301 let s = String::deserialize(deserializer)?;
302 Ok(Self::from(s.as_str()))
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Hash)]
308pub enum ApiKeySource {
309 None,
311 User,
312 Project,
313 Org,
314 Temporary,
315 Oauth,
316 Unknown(String),
318}
319
320impl ApiKeySource {
321 pub fn as_str(&self) -> &str {
322 match self {
323 Self::None => "none",
324 Self::User => "user",
325 Self::Project => "project",
326 Self::Org => "org",
327 Self::Temporary => "temporary",
328 Self::Oauth => "oauth",
329 Self::Unknown(s) => s.as_str(),
330 }
331 }
332}
333
334impl fmt::Display for ApiKeySource {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 f.write_str(self.as_str())
337 }
338}
339
340impl From<&str> for ApiKeySource {
341 fn from(s: &str) -> Self {
342 match s {
343 "none" => Self::None,
344 "user" => Self::User,
345 "project" => Self::Project,
346 "org" => Self::Org,
347 "temporary" => Self::Temporary,
348 "oauth" => Self::Oauth,
349 other => Self::Unknown(other.to_string()),
350 }
351 }
352}
353
354impl Serialize for ApiKeySource {
355 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
356 serializer.serialize_str(self.as_str())
357 }
358}
359
360impl<'de> Deserialize<'de> for ApiKeySource {
361 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
362 let s = String::deserialize(deserializer)?;
363 Ok(Self::from(s.as_str()))
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Hash)]
369pub enum OutputStyle {
370 Default,
372 Unknown(String),
374}
375
376impl OutputStyle {
377 pub fn as_str(&self) -> &str {
378 match self {
379 Self::Default => "default",
380 Self::Unknown(s) => s.as_str(),
381 }
382 }
383}
384
385impl fmt::Display for OutputStyle {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 f.write_str(self.as_str())
388 }
389}
390
391impl From<&str> for OutputStyle {
392 fn from(s: &str) -> Self {
393 match s {
394 "default" => Self::Default,
395 other => Self::Unknown(other.to_string()),
396 }
397 }
398}
399
400impl Serialize for OutputStyle {
401 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
402 serializer.serialize_str(self.as_str())
403 }
404}
405
406impl<'de> Deserialize<'de> for OutputStyle {
407 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
408 let s = String::deserialize(deserializer)?;
409 Ok(Self::from(s.as_str()))
410 }
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Hash)]
415pub enum InitPermissionMode {
416 Default,
418 AcceptEdits,
419 BypassPermissions,
420 Plan,
421 DontAsk,
422 Auto,
423 Unknown(String),
425}
426
427impl InitPermissionMode {
428 pub fn as_str(&self) -> &str {
429 match self {
430 Self::Default => "default",
431 Self::AcceptEdits => "acceptEdits",
432 Self::BypassPermissions => "bypassPermissions",
433 Self::Plan => "plan",
434 Self::DontAsk => "dontAsk",
435 Self::Auto => "auto",
436 Self::Unknown(s) => s.as_str(),
437 }
438 }
439}
440
441impl fmt::Display for InitPermissionMode {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 f.write_str(self.as_str())
444 }
445}
446
447impl From<&str> for InitPermissionMode {
448 fn from(s: &str) -> Self {
449 match s {
450 "default" => Self::Default,
451 "acceptEdits" => Self::AcceptEdits,
452 "bypassPermissions" => Self::BypassPermissions,
453 "plan" => Self::Plan,
454 "dontAsk" => Self::DontAsk,
455 "auto" => Self::Auto,
456 other => Self::Unknown(other.to_string()),
457 }
458 }
459}
460
461impl Serialize for InitPermissionMode {
462 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
463 serializer.serialize_str(self.as_str())
464 }
465}
466
467impl<'de> Deserialize<'de> for InitPermissionMode {
468 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
469 let s = String::deserialize(deserializer)?;
470 Ok(Self::from(s.as_str()))
471 }
472}
473
474#[derive(Debug, Clone, PartialEq, Eq, Hash)]
476pub enum StatusMessageStatus {
477 Compacting,
479 Requesting,
481 Unknown(String),
483}
484
485impl StatusMessageStatus {
486 pub fn as_str(&self) -> &str {
487 match self {
488 Self::Compacting => "compacting",
489 Self::Requesting => "requesting",
490 Self::Unknown(s) => s.as_str(),
491 }
492 }
493}
494
495impl fmt::Display for StatusMessageStatus {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 f.write_str(self.as_str())
498 }
499}
500
501impl From<&str> for StatusMessageStatus {
502 fn from(s: &str) -> Self {
503 match s {
504 "compacting" => Self::Compacting,
505 "requesting" => Self::Requesting,
506 other => Self::Unknown(other.to_string()),
507 }
508 }
509}
510
511impl Serialize for StatusMessageStatus {
512 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
513 serializer.serialize_str(self.as_str())
514 }
515}
516
517impl<'de> Deserialize<'de> for StatusMessageStatus {
518 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
519 let s = String::deserialize(deserializer)?;
520 Ok(Self::from(s.as_str()))
521 }
522}
523
524pub(crate) fn serialize_optional_uuid<S>(
526 uuid: &Option<Uuid>,
527 serializer: S,
528) -> Result<S::Ok, S::Error>
529where
530 S: Serializer,
531{
532 match uuid {
533 Some(id) => serializer.serialize_str(&id.to_string()),
534 None => serializer.serialize_none(),
535 }
536}
537
538pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
540where
541 D: Deserializer<'de>,
542{
543 let opt_str: Option<String> = Option::deserialize(deserializer)?;
544 match opt_str {
545 Some(s) => Uuid::parse_str(&s)
546 .map(Some)
547 .map_err(serde::de::Error::custom),
548 None => Ok(None),
549 }
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
555pub struct MessageOrigin {
556 pub kind: String,
557 #[serde(flatten)]
558 pub extra: serde_json::Map<String, Value>,
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
563pub struct SummarizeMetadata {
564 pub messages_summarized: u64,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub user_context: Option<String>,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub direction: Option<String>,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
573pub struct McpMeta {
574 #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
575 pub meta: Option<Value>,
576 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub structured_content: Option<Value>,
578 #[serde(default, skip_serializing_if = "Vec::is_empty")]
583 pub resource_links: Vec<ResourceLink>,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
590pub struct ResourceLink {
591 pub uri: String,
592 pub name: String,
593 #[serde(default, skip_serializing_if = "Option::is_none")]
594 pub title: Option<String>,
595 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub description: Option<String>,
597 #[serde(default, skip_serializing_if = "Option::is_none", rename = "mimeType")]
598 pub mime_type: Option<String>,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
600 pub size: Option<u64>,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub annotations: Option<Value>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
607pub struct ToolResultMeta {
608 pub id: String,
610 pub non_execution_kind: String,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub user_feedback: Option<String>,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct UserMessage {
624 pub message: MessageContent,
625 #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
626 #[serde(
627 serialize_with = "serialize_optional_uuid",
628 deserialize_with = "deserialize_optional_uuid"
629 )]
630 pub session_id: Option<Uuid>,
631 #[serde(skip_serializing_if = "Option::is_none")]
633 pub parent_tool_use_id: Option<String>,
634 #[serde(skip_serializing_if = "Option::is_none")]
636 pub uuid: Option<String>,
637 #[serde(skip_serializing_if = "Option::is_none")]
639 pub timestamp: Option<String>,
640 #[serde(skip_serializing_if = "Option::is_none")]
647 pub tool_use_result: Option<serde_json::Value>,
648 #[serde(skip_serializing_if = "Option::is_none")]
651 pub subagent_type: Option<String>,
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub task_description: Option<String>,
655 #[serde(skip_serializing_if = "Option::is_none")]
656 pub origin: Option<MessageOrigin>,
657 #[serde(skip_serializing_if = "Option::is_none")]
658 pub priority: Option<String>,
659 #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
660 pub is_synthetic: Option<bool>,
661 #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
662 pub should_query: Option<bool>,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub is_meta: Option<bool>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub is_visible_in_transcript_only: Option<bool>,
667 #[serde(default, skip_serializing_if = "Option::is_none")]
668 pub is_virtual: Option<bool>,
669 #[serde(default, skip_serializing_if = "Option::is_none")]
670 pub is_compact_summary: Option<bool>,
671 #[serde(skip_serializing_if = "Option::is_none")]
672 pub summarize_metadata: Option<SummarizeMetadata>,
673 #[serde(skip_serializing_if = "Option::is_none")]
674 pub mcp_meta: Option<McpMeta>,
675 #[serde(skip_serializing_if = "Option::is_none")]
678 pub tool_result_meta: Option<Vec<ToolResultMeta>>,
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub source_tool_use_id: Option<String>,
681 #[serde(skip_serializing_if = "Option::is_none")]
682 pub source_tool_assistant_uuid: Option<String>,
683 #[serde(skip_serializing_if = "Option::is_none")]
684 pub image_paste_ids: Option<Vec<u64>>,
685 #[serde(skip_serializing_if = "Option::is_none")]
686 pub client_platform: Option<String>,
687 #[serde(skip_serializing_if = "Option::is_none")]
688 pub inbound_origin: Option<String>,
689 #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
690 pub is_replay: Option<bool>,
691 #[serde(skip_serializing_if = "Option::is_none")]
692 pub file_attachments: Option<Vec<Value>>,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
695 pub seeded_summon: Option<bool>,
696 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub client_composed: Option<bool>,
700}
701
702impl UserMessage {
703 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
711 &self,
712 ) -> Option<Result<T, serde_json::Error>> {
713 self.tool_use_result
714 .as_ref()
715 .map(|v| serde_json::from_value(v.clone()))
716 }
717
718 pub fn subagent_result(&self) -> Option<SubagentResult> {
729 self.tool_use_result
730 .as_ref()
731 .and_then(|v| serde_json::from_value(v.clone()).ok())
732 }
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct SubagentResult {
747 #[serde(skip_serializing_if = "Option::is_none")]
749 pub status: Option<String>,
750 #[serde(skip_serializing_if = "Option::is_none")]
752 pub prompt: Option<String>,
753 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
755 pub agent_id: Option<String>,
756 #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
758 pub agent_type: Option<String>,
759 #[serde(
761 default,
762 deserialize_with = "deserialize_content_blocks",
763 skip_serializing_if = "Vec::is_empty"
764 )]
765 pub content: Vec<ContentBlock>,
766 #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
768 pub resolved_model: Option<String>,
769 #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
771 pub total_duration_ms: Option<u64>,
772 #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
774 pub total_tokens: Option<u64>,
775 #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
777 pub total_tool_use_count: Option<u64>,
778 #[serde(skip_serializing_if = "Option::is_none")]
780 pub usage: Option<super::result::UsageInfo>,
781 #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
783 pub tool_stats: Option<SubagentToolStats>,
784}
785
786#[derive(Debug, Clone, Default, Serialize, Deserialize)]
791#[serde(rename_all = "camelCase")]
792pub struct SubagentToolStats {
793 #[serde(default)]
794 pub read_count: u64,
795 #[serde(default)]
796 pub search_count: u64,
797 #[serde(default)]
798 pub bash_count: u64,
799 #[serde(default)]
800 pub edit_file_count: u64,
801 #[serde(default)]
802 pub lines_added: u64,
803 #[serde(default)]
804 pub lines_removed: u64,
805 #[serde(default)]
806 pub other_tool_count: u64,
807 #[serde(flatten)]
808 pub extra: serde_json::Map<String, Value>,
809}
810
811#[derive(Debug, Clone, Default, PartialEq, Eq)]
838pub struct SubagentUsageRollup {
839 pub subagent_tokens: u64,
842 pub agent_count: u32,
844 pub tool_uses: u64,
846 pub duration_ms: u64,
848 seen_agent_ids: std::collections::BTreeSet<String>,
849}
850
851impl SubagentUsageRollup {
852 pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
858 match output {
859 ClaudeOutput::User(user) => self.observe_user(user),
860 _ => false,
861 }
862 }
863
864 pub fn observe_user(&mut self, user: &UserMessage) -> bool {
871 let Some(result) = user.subagent_result() else {
872 return false;
873 };
874 if result.total_tokens.is_none() && result.agent_id.is_none() {
875 return false;
876 }
877 if let Some(agent_id) = &result.agent_id {
878 if !self.seen_agent_ids.insert(agent_id.clone()) {
879 return false;
880 }
881 }
882 self.agent_count += 1;
883 self.subagent_tokens += result.total_tokens.unwrap_or(0);
884 self.tool_uses += result.total_tool_use_count.unwrap_or(0);
885 self.duration_ms += result.total_duration_ms.unwrap_or(0);
886 true
887 }
888}
889
890#[derive(Debug, Clone, Serialize, Deserialize)]
892pub struct MessageContent {
893 pub role: MessageRole,
894 #[serde(deserialize_with = "deserialize_content_blocks")]
895 pub content: Vec<ContentBlock>,
896}
897
898#[derive(Debug, Clone, Serialize, Deserialize)]
900pub struct SystemMessage {
901 pub subtype: SystemSubtype,
902 #[serde(flatten)]
903 pub data: Value, }
905
906impl SystemMessage {
907 pub fn is_init(&self) -> bool {
909 self.subtype == SystemSubtype::Init
910 }
911
912 pub fn is_status(&self) -> bool {
914 self.subtype == SystemSubtype::Status
915 }
916
917 pub fn is_compact_boundary(&self) -> bool {
919 self.subtype == SystemSubtype::CompactBoundary
920 }
921
922 pub fn as_init(&self) -> Option<InitMessage> {
924 if self.subtype != SystemSubtype::Init {
925 return None;
926 }
927 serde_json::from_value(self.data.clone()).ok()
928 }
929
930 pub fn as_status(&self) -> Option<StatusMessage> {
932 if self.subtype != SystemSubtype::Status {
933 return None;
934 }
935 serde_json::from_value(self.data.clone()).ok()
936 }
937
938 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
940 if self.subtype != SystemSubtype::CompactBoundary {
941 return None;
942 }
943 serde_json::from_value(self.data.clone()).ok()
944 }
945
946 pub fn is_task_started(&self) -> bool {
948 self.subtype == SystemSubtype::TaskStarted
949 }
950
951 pub fn is_task_progress(&self) -> bool {
953 self.subtype == SystemSubtype::TaskProgress
954 }
955
956 pub fn is_task_notification(&self) -> bool {
958 self.subtype == SystemSubtype::TaskNotification
959 }
960
961 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
963 if self.subtype != SystemSubtype::TaskStarted {
964 return None;
965 }
966 serde_json::from_value(self.data.clone()).ok()
967 }
968
969 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
971 if self.subtype != SystemSubtype::TaskProgress {
972 return None;
973 }
974 serde_json::from_value(self.data.clone()).ok()
975 }
976
977 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
979 if self.subtype != SystemSubtype::TaskNotification {
980 return None;
981 }
982 serde_json::from_value(self.data.clone()).ok()
983 }
984
985 pub fn is_task_updated(&self) -> bool {
987 self.subtype == SystemSubtype::TaskUpdated
988 }
989
990 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
992 if self.subtype != SystemSubtype::TaskUpdated {
993 return None;
994 }
995 serde_json::from_value(self.data.clone()).ok()
996 }
997
998 pub fn is_thinking_tokens(&self) -> bool {
1000 self.subtype == SystemSubtype::ThinkingTokens
1001 }
1002
1003 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
1005 if self.subtype != SystemSubtype::ThinkingTokens {
1006 return None;
1007 }
1008 serde_json::from_value(self.data.clone()).ok()
1009 }
1010
1011 pub fn is_code_change_published(&self) -> bool {
1013 self.subtype == SystemSubtype::CodeChangePublished
1014 }
1015
1016 pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
1018 if self.subtype != SystemSubtype::CodeChangePublished {
1019 return None;
1020 }
1021 serde_json::from_value(self.data.clone()).ok()
1022 }
1023
1024 pub fn is_vcs_state_changed(&self) -> bool {
1026 self.subtype == SystemSubtype::VcsStateChanged
1027 }
1028
1029 pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
1031 if self.subtype != SystemSubtype::VcsStateChanged {
1032 return None;
1033 }
1034 serde_json::from_value(self.data.clone()).ok()
1035 }
1036
1037 pub fn is_feedback_draft_queued(&self) -> bool {
1039 self.subtype == SystemSubtype::FeedbackDraftQueued
1040 }
1041
1042 pub fn as_feedback_draft_queued(&self) -> Option<FeedbackDraftQueuedMessage> {
1044 if self.subtype != SystemSubtype::FeedbackDraftQueued {
1045 return None;
1046 }
1047 serde_json::from_value(self.data.clone()).ok()
1048 }
1049
1050 pub fn is_cloud_session_delta(&self) -> bool {
1052 self.subtype == SystemSubtype::CloudSessionDelta
1053 }
1054
1055 pub fn as_cloud_session_delta(&self) -> Option<CloudSessionDeltaMessage> {
1057 if self.subtype != SystemSubtype::CloudSessionDelta {
1058 return None;
1059 }
1060 serde_json::from_value(self.data.clone()).ok()
1061 }
1062
1063 pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
1065 macro_rules! parse {
1066 ($variant:ident, $ty:ty) => {
1067 serde_json::from_value::<$ty>(self.data.clone())
1068 .ok()
1069 .map(KnownSystemEvent::$variant)
1070 };
1071 }
1072
1073 match self.subtype {
1074 SystemSubtype::Init => parse!(Init, InitMessage),
1075 SystemSubtype::Status => parse!(Status, StatusMessage),
1076 SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
1077 SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
1078 SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
1079 SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
1080 SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
1081 SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
1082 SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
1083 SystemSubtype::ControlRequestProgress => {
1084 parse!(ControlRequestProgress, ControlRequestProgressMessage)
1085 }
1086 SystemSubtype::ModelRefusalFallback => {
1087 parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
1088 }
1089 SystemSubtype::ModelRefusalNoFallback => {
1090 parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
1091 }
1092 SystemSubtype::LocalCommandOutput => {
1093 parse!(LocalCommandOutput, LocalCommandOutputMessage)
1094 }
1095 SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
1096 SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
1097 SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
1098 SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
1099 SystemSubtype::BackgroundTasksChanged => {
1100 parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
1101 }
1102 SystemSubtype::SessionStateChanged => {
1103 parse!(SessionStateChanged, SessionStateChangedMessage)
1104 }
1105 SystemSubtype::WorkerShuttingDown => {
1106 parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
1107 }
1108 SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
1109 SystemSubtype::Notification => parse!(Notification, NotificationMessage),
1110 SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
1111 SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
1112 SystemSubtype::ElicitationComplete => {
1113 parse!(ElicitationComplete, ElicitationCompleteMessage)
1114 }
1115 SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1116 SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1117 SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1118 SystemSubtype::CodeChangePublished => {
1119 parse!(CodeChangePublished, CodeChangePublishedMessage)
1120 }
1121 SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
1122 SystemSubtype::FeedbackDraftQueued => {
1123 parse!(FeedbackDraftQueued, FeedbackDraftQueuedMessage)
1124 }
1125 SystemSubtype::CloudSessionDelta => {
1126 parse!(CloudSessionDelta, CloudSessionDeltaMessage)
1127 }
1128 SystemSubtype::Unknown(_) => None,
1129 }
1130 }
1131
1132 pub fn typed_value(&self) -> Option<Value> {
1141 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1142 parsed.and_then(|v| serde_json::to_value(v).ok())
1143 }
1144 match self.subtype {
1145 SystemSubtype::Init => reserialize(self.as_init()),
1146 SystemSubtype::Status => reserialize(self.as_status()),
1147 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1148 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1149 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1150 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1151 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1152 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1153 SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1154 SystemSubtype::ControlRequestProgress => {
1155 reserialize(parse_system::<ControlRequestProgressMessage>(self))
1156 }
1157 SystemSubtype::ModelRefusalFallback => {
1158 reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1159 }
1160 SystemSubtype::ModelRefusalNoFallback => {
1161 reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1162 }
1163 SystemSubtype::LocalCommandOutput => {
1164 reserialize(parse_system::<LocalCommandOutputMessage>(self))
1165 }
1166 SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1167 SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1168 SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1169 SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1170 SystemSubtype::BackgroundTasksChanged => {
1171 reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1172 }
1173 SystemSubtype::SessionStateChanged => {
1174 reserialize(parse_system::<SessionStateChangedMessage>(self))
1175 }
1176 SystemSubtype::WorkerShuttingDown => {
1177 reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1178 }
1179 SystemSubtype::CommandsChanged => {
1180 reserialize(parse_system::<CommandsChangedMessage>(self))
1181 }
1182 SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1183 SystemSubtype::FilesPersisted => {
1184 reserialize(parse_system::<FilesPersistedMessage>(self))
1185 }
1186 SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1187 SystemSubtype::ElicitationComplete => {
1188 reserialize(parse_system::<ElicitationCompleteMessage>(self))
1189 }
1190 SystemSubtype::PermissionDenied => {
1191 reserialize(parse_system::<PermissionDeniedMessage>(self))
1192 }
1193 SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1194 SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1195 SystemSubtype::CodeChangePublished => {
1196 reserialize(parse_system::<CodeChangePublishedMessage>(self))
1197 }
1198 SystemSubtype::VcsStateChanged => {
1199 reserialize(parse_system::<VcsStateChangedMessage>(self))
1200 }
1201 SystemSubtype::FeedbackDraftQueued => {
1202 reserialize(parse_system::<FeedbackDraftQueuedMessage>(self))
1203 }
1204 SystemSubtype::CloudSessionDelta => {
1205 reserialize(parse_system::<CloudSessionDeltaMessage>(self))
1206 }
1207 SystemSubtype::Unknown(_) => None,
1208 }
1209 }
1210}
1211
1212fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1213 serde_json::from_value(message.data.clone()).ok()
1214}
1215
1216#[allow(clippy::large_enum_variant)]
1222#[derive(Debug, Clone, Serialize, Deserialize)]
1223pub enum KnownSystemEvent {
1224 Init(InitMessage),
1225 Status(StatusMessage),
1226 CompactBoundary(CompactBoundaryMessage),
1227 ThinkingTokens(ThinkingTokensMessage),
1228 TaskStarted(TaskStartedMessage),
1229 TaskProgress(TaskProgressMessage),
1230 TaskUpdated(TaskUpdatedMessage),
1231 TaskNotification(TaskNotificationMessage),
1232 ApiRetry(ApiRetryMessage),
1233 ControlRequestProgress(ControlRequestProgressMessage),
1234 ModelRefusalFallback(ModelRefusalFallbackMessage),
1235 ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1236 LocalCommandOutput(LocalCommandOutputMessage),
1237 HookStarted(HookStartedMessage),
1238 HookProgress(HookProgressMessage),
1239 HookResponse(HookResponseMessage),
1240 PluginInstall(PluginInstallMessage),
1241 BackgroundTasksChanged(BackgroundTasksChangedMessage),
1242 SessionStateChanged(SessionStateChangedMessage),
1243 WorkerShuttingDown(WorkerShuttingDownMessage),
1244 CommandsChanged(CommandsChangedMessage),
1245 Notification(NotificationMessage),
1246 FilesPersisted(FilesPersistedMessage),
1247 MemoryRecall(MemoryRecallMessage),
1248 ElicitationComplete(ElicitationCompleteMessage),
1249 PermissionDenied(PermissionDeniedMessage),
1250 MirrorError(MirrorErrorMessage),
1251 Informational(InformationalMessage),
1252 CodeChangePublished(CodeChangePublishedMessage),
1253 VcsStateChanged(VcsStateChangedMessage),
1254 FeedbackDraftQueued(FeedbackDraftQueuedMessage),
1255 CloudSessionDelta(CloudSessionDeltaMessage),
1256}
1257
1258#[derive(Debug, Clone, Serialize, Deserialize)]
1259pub struct ApiRetryMessage {
1260 pub attempt: u64,
1261 pub max_retries: u64,
1262 pub retry_delay_ms: u64,
1263 pub error_status: Option<u16>,
1264 pub error: String,
1265 #[serde(default, skip_serializing_if = "Option::is_none")]
1270 pub no_response: Option<ApiRetryNoResponse>,
1271 #[serde(default, skip_serializing_if = "Option::is_none")]
1272 pub uuid: Option<String>,
1273 #[serde(default, skip_serializing_if = "Option::is_none")]
1274 pub session_id: Option<String>,
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1280pub struct ApiRetryNoResponse {
1281 pub waited_ms: u64,
1283 pub retry_wait_ms: u64,
1285}
1286
1287#[derive(Debug, Clone, Serialize, Deserialize)]
1288pub struct ControlRequestProgressMessage {
1289 pub request_id: String,
1290 pub status: String,
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1292 pub attempt: Option<u64>,
1293 #[serde(default, skip_serializing_if = "Option::is_none")]
1294 pub max_retries: Option<u64>,
1295 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub retry_delay_ms: Option<u64>,
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1298 pub error_status: Option<u16>,
1299 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub error: Option<String>,
1301 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub uuid: Option<String>,
1303 #[serde(default, skip_serializing_if = "Option::is_none")]
1304 pub session_id: Option<String>,
1305}
1306
1307#[derive(Debug, Clone, Serialize, Deserialize)]
1308pub struct ModelRefusalFallbackMessage {
1309 pub trigger: String,
1310 pub direction: String,
1311 #[serde(default, skip_serializing_if = "Option::is_none")]
1317 pub scope: Option<RefusalFallbackScope>,
1318 pub original_model: String,
1319 pub fallback_model: String,
1320 pub request_id: Option<String>,
1321 #[serde(default, skip_serializing_if = "Option::is_none")]
1322 pub api_refusal_category: Option<String>,
1323 #[serde(default, skip_serializing_if = "Option::is_none")]
1328 pub saw_cyber_refusal: Option<bool>,
1329 #[serde(default, skip_serializing_if = "Option::is_none")]
1330 pub api_refusal_explanation: Option<String>,
1331 #[serde(default, skip_serializing_if = "Option::is_none")]
1332 pub retracted_message_uuids: Option<Vec<String>>,
1333 #[serde(default, skip_serializing_if = "Option::is_none")]
1334 pub refused_user_message_uuid: Option<String>,
1335 pub content: Value,
1336 #[serde(default, skip_serializing_if = "Option::is_none")]
1337 pub uuid: Option<String>,
1338 #[serde(default, skip_serializing_if = "Option::is_none")]
1339 pub session_id: Option<String>,
1340}
1341
1342#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1346pub enum RefusalFallbackScope {
1347 Session,
1349 Local,
1352 Unknown(String),
1354}
1355
1356impl RefusalFallbackScope {
1357 pub fn as_str(&self) -> &str {
1358 match self {
1359 Self::Session => "session",
1360 Self::Local => "local",
1361 Self::Unknown(s) => s.as_str(),
1362 }
1363 }
1364}
1365
1366impl fmt::Display for RefusalFallbackScope {
1367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1368 f.write_str(self.as_str())
1369 }
1370}
1371
1372impl From<&str> for RefusalFallbackScope {
1373 fn from(s: &str) -> Self {
1374 match s {
1375 "session" => Self::Session,
1376 "local" => Self::Local,
1377 other => Self::Unknown(other.to_string()),
1378 }
1379 }
1380}
1381
1382impl Serialize for RefusalFallbackScope {
1383 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1384 serializer.serialize_str(self.as_str())
1385 }
1386}
1387
1388impl<'de> Deserialize<'de> for RefusalFallbackScope {
1389 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1390 let s = String::deserialize(deserializer)?;
1391 Ok(Self::from(s.as_str()))
1392 }
1393}
1394
1395#[derive(Debug, Clone, Serialize, Deserialize)]
1396pub struct ModelRefusalNoFallbackMessage {
1397 pub original_model: String,
1398 pub request_id: Option<String>,
1399 #[serde(default, skip_serializing_if = "Option::is_none")]
1400 pub api_refusal_category: Option<String>,
1401 #[serde(default, skip_serializing_if = "Option::is_none")]
1402 pub api_refusal_explanation: Option<String>,
1403 pub content: Value,
1404 #[serde(default, skip_serializing_if = "Option::is_none")]
1405 pub uuid: Option<String>,
1406 #[serde(default, skip_serializing_if = "Option::is_none")]
1407 pub session_id: Option<String>,
1408}
1409
1410#[derive(Debug, Clone, Serialize, Deserialize)]
1411pub struct LocalCommandOutputMessage {
1412 pub content: String,
1413 #[serde(default, skip_serializing_if = "Option::is_none")]
1414 pub uuid: Option<String>,
1415 #[serde(default, skip_serializing_if = "Option::is_none")]
1416 pub session_id: Option<String>,
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct HookStartedMessage {
1421 pub hook_id: String,
1422 pub hook_name: String,
1423 pub hook_event: String,
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1425 pub uuid: Option<String>,
1426 #[serde(default, skip_serializing_if = "Option::is_none")]
1427 pub session_id: Option<String>,
1428}
1429
1430#[derive(Debug, Clone, Serialize, Deserialize)]
1431pub struct HookProgressMessage {
1432 pub hook_id: String,
1433 pub hook_name: String,
1434 pub hook_event: String,
1435 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 pub stdout: Option<String>,
1437 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub stderr: Option<String>,
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1440 pub output: Option<String>,
1441 #[serde(default, skip_serializing_if = "Option::is_none")]
1442 pub uuid: Option<String>,
1443 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 pub session_id: Option<String>,
1445}
1446
1447#[derive(Debug, Clone, Serialize, Deserialize)]
1448pub struct HookResponseMessage {
1449 pub hook_id: String,
1450 pub hook_name: String,
1451 pub hook_event: String,
1452 #[serde(default, skip_serializing_if = "Option::is_none")]
1453 pub stdout: Option<String>,
1454 #[serde(default, skip_serializing_if = "Option::is_none")]
1455 pub stderr: Option<String>,
1456 #[serde(default, skip_serializing_if = "Option::is_none")]
1457 pub output: Option<String>,
1458 #[serde(default, skip_serializing_if = "Option::is_none")]
1459 pub exit_code: Option<i32>,
1460 pub outcome: String,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1462 pub uuid: Option<String>,
1463 #[serde(default, skip_serializing_if = "Option::is_none")]
1464 pub session_id: Option<String>,
1465}
1466
1467#[derive(Debug, Clone, Serialize, Deserialize)]
1468pub struct PluginInstallMessage {
1469 pub status: String,
1470 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub name: Option<String>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub error: Option<String>,
1474 #[serde(default, skip_serializing_if = "Option::is_none")]
1475 pub uuid: Option<String>,
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1477 pub session_id: Option<String>,
1478}
1479
1480#[derive(Debug, Clone, Serialize, Deserialize)]
1481pub struct BackgroundTasksChangedMessage {
1482 pub tasks: Vec<BackgroundTaskInfo>,
1483 #[serde(default, skip_serializing_if = "Option::is_none")]
1484 pub uuid: Option<String>,
1485 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 pub session_id: Option<String>,
1487}
1488
1489#[derive(Debug, Clone, Serialize, Deserialize)]
1490pub struct BackgroundTaskInfo {
1491 pub task_id: String,
1492 pub task_type: String,
1493 pub description: String,
1494 #[serde(default, skip_serializing_if = "Option::is_none")]
1497 pub ambient: Option<bool>,
1498}
1499
1500#[derive(Debug, Clone, Serialize, Deserialize)]
1501pub struct SessionStateChangedMessage {
1502 pub state: String,
1503 #[serde(default, skip_serializing_if = "Option::is_none")]
1504 pub uuid: Option<String>,
1505 #[serde(default, skip_serializing_if = "Option::is_none")]
1506 pub session_id: Option<String>,
1507}
1508
1509#[derive(Debug, Clone, Serialize, Deserialize)]
1510pub struct WorkerShuttingDownMessage {
1511 pub reason: String,
1512 #[serde(default, skip_serializing_if = "Option::is_none")]
1513 pub uuid: Option<String>,
1514 #[serde(default, skip_serializing_if = "Option::is_none")]
1515 pub session_id: Option<String>,
1516}
1517
1518#[derive(Debug, Clone, Serialize, Deserialize)]
1519pub struct CommandsChangedMessage {
1520 pub commands: Vec<CommandInfo>,
1521 #[serde(default, skip_serializing_if = "Option::is_none")]
1522 pub uuid: Option<String>,
1523 #[serde(default, skip_serializing_if = "Option::is_none")]
1524 pub session_id: Option<String>,
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize)]
1528pub struct CommandInfo {
1529 pub name: String,
1530 pub description: String,
1531 #[serde(rename = "argumentHint")]
1532 pub argument_hint: String,
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub aliases: Option<Vec<String>>,
1535}
1536
1537#[derive(Debug, Clone, Serialize, Deserialize)]
1538pub struct NotificationMessage {
1539 pub key: String,
1540 pub text: String,
1541 pub priority: String,
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub color: Option<String>,
1544 #[serde(default, skip_serializing_if = "Option::is_none")]
1545 pub timeout_ms: Option<u64>,
1546 #[serde(default, skip_serializing_if = "Option::is_none")]
1547 pub uuid: Option<String>,
1548 #[serde(default, skip_serializing_if = "Option::is_none")]
1549 pub session_id: Option<String>,
1550}
1551
1552#[derive(Debug, Clone, Serialize, Deserialize)]
1553pub struct FilesPersistedMessage {
1554 pub files: Vec<PersistedFile>,
1555 pub failed: Vec<FailedPersistedFile>,
1556 pub processed_at: String,
1557 #[serde(default, skip_serializing_if = "Option::is_none")]
1558 pub uuid: Option<String>,
1559 #[serde(default, skip_serializing_if = "Option::is_none")]
1560 pub session_id: Option<String>,
1561}
1562
1563#[derive(Debug, Clone, Serialize, Deserialize)]
1564pub struct PersistedFile {
1565 pub filename: String,
1566 pub file_id: String,
1567}
1568
1569#[derive(Debug, Clone, Serialize, Deserialize)]
1570pub struct FailedPersistedFile {
1571 pub filename: String,
1572 pub error: String,
1573}
1574
1575#[derive(Debug, Clone, Serialize, Deserialize)]
1576pub struct MemoryRecallMessage {
1577 pub mode: String,
1578 pub memories: Vec<MemoryRecallItem>,
1579 #[serde(default, skip_serializing_if = "Option::is_none")]
1580 pub uuid: Option<String>,
1581 #[serde(default, skip_serializing_if = "Option::is_none")]
1582 pub session_id: Option<String>,
1583}
1584
1585#[derive(Debug, Clone, Serialize, Deserialize)]
1586pub struct MemoryRecallItem {
1587 pub path: String,
1588 pub scope: String,
1589 #[serde(default, skip_serializing_if = "Option::is_none")]
1590 pub content: Option<String>,
1591}
1592
1593#[derive(Debug, Clone, Serialize, Deserialize)]
1594pub struct ElicitationCompleteMessage {
1595 pub mcp_server_name: String,
1596 pub elicitation_id: String,
1597 #[serde(default, skip_serializing_if = "Option::is_none")]
1598 pub uuid: Option<String>,
1599 #[serde(default, skip_serializing_if = "Option::is_none")]
1600 pub session_id: Option<String>,
1601}
1602
1603#[derive(Debug, Clone, Serialize, Deserialize)]
1604pub struct PermissionDeniedMessage {
1605 pub tool_name: String,
1606 pub tool_use_id: String,
1607 #[serde(default, skip_serializing_if = "Option::is_none")]
1608 pub agent_id: Option<String>,
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1610 pub decision_reason_type: Option<String>,
1611 #[serde(default, skip_serializing_if = "Option::is_none")]
1612 pub decision_reason: Option<String>,
1613 pub message: String,
1614 #[serde(default, skip_serializing_if = "Option::is_none")]
1615 pub uuid: Option<String>,
1616 #[serde(default, skip_serializing_if = "Option::is_none")]
1617 pub session_id: Option<String>,
1618}
1619
1620#[derive(Debug, Clone, Serialize, Deserialize)]
1621pub struct MirrorErrorMessage {
1622 pub error: String,
1623 pub key: MirrorErrorKey,
1624 #[serde(default, skip_serializing_if = "Option::is_none")]
1625 pub uuid: Option<String>,
1626 #[serde(default, skip_serializing_if = "Option::is_none")]
1627 pub session_id: Option<String>,
1628}
1629
1630#[derive(Debug, Clone, Serialize, Deserialize)]
1631pub struct MirrorErrorKey {
1632 #[serde(rename = "projectKey")]
1633 pub project_key: String,
1634 #[serde(rename = "sessionId")]
1635 pub session_id: String,
1636 #[serde(default, skip_serializing_if = "Option::is_none")]
1637 pub subpath: Option<String>,
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize)]
1641pub struct InformationalMessage {
1642 pub content: String,
1643 pub level: String,
1644 #[serde(default, skip_serializing_if = "Option::is_none")]
1645 pub tool_use_id: Option<String>,
1646 #[serde(default, skip_serializing_if = "Option::is_none")]
1647 pub prevent_continuation: Option<bool>,
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)]
1656pub struct PluginInfo {
1657 pub name: String,
1659 pub path: String,
1661 #[serde(skip_serializing_if = "Option::is_none")]
1663 pub source: Option<String>,
1664 #[serde(default, skip_serializing_if = "Option::is_none")]
1666 pub version: Option<String>,
1667}
1668
1669#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1671pub struct PluginDiagnostic {
1672 pub plugin: String,
1673 #[serde(rename = "type")]
1674 pub diagnostic_type: String,
1675 pub message: String,
1676}
1677
1678#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1680pub struct MemoryPaths {
1681 #[serde(default, skip_serializing_if = "Option::is_none")]
1682 pub auto: Option<String>,
1683 #[serde(default, skip_serializing_if = "Option::is_none")]
1684 pub team: Option<String>,
1685 #[serde(flatten)]
1686 pub extra: serde_json::Map<String, Value>,
1687}
1688
1689#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1693pub struct McpServerError {
1694 pub name: String,
1695 #[serde(rename = "type")]
1697 pub error_type: String,
1698 pub message: String,
1699}
1700
1701#[derive(Debug, Clone, Serialize, Deserialize)]
1703pub struct InitMessage {
1704 pub session_id: String,
1706 #[serde(skip_serializing_if = "Option::is_none")]
1708 pub cwd: Option<String>,
1709 #[serde(skip_serializing_if = "Option::is_none")]
1711 pub model: Option<String>,
1712 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1714 pub tools: Vec<String>,
1715 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1717 pub mcp_servers: Vec<Value>,
1718 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1720 pub slash_commands: Vec<String>,
1721 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1724 pub terminal_slash_commands: Vec<String>,
1725 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1727 pub agents: Vec<String>,
1728 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1730 pub plugins: Vec<PluginInfo>,
1731 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1733 pub skills: Vec<Value>,
1734 #[serde(skip_serializing_if = "Option::is_none")]
1736 pub claude_code_version: Option<String>,
1737 #[serde(skip_serializing_if = "Option::is_none")]
1740 pub messaging_socket_path: Option<String>,
1741 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1743 pub api_key_source: Option<ApiKeySource>,
1744 #[serde(skip_serializing_if = "Option::is_none")]
1746 pub output_style: Option<OutputStyle>,
1747 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1749 pub permission_mode: Option<InitPermissionMode>,
1750
1751 #[serde(skip_serializing_if = "Option::is_none")]
1753 pub uuid: Option<String>,
1754
1755 #[serde(skip_serializing_if = "Option::is_none")]
1757 pub memory_paths: Option<MemoryPaths>,
1758
1759 #[serde(skip_serializing_if = "Option::is_none")]
1761 pub fast_mode_state: Option<String>,
1762
1763 #[serde(default, skip_serializing_if = "Option::is_none")]
1765 pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
1766
1767 #[serde(default, skip_serializing_if = "Option::is_none")]
1770 pub mcp_server_errors: Option<Vec<McpServerError>>,
1771
1772 #[serde(default, skip_serializing_if = "Option::is_none")]
1774 pub analytics_disabled: Option<bool>,
1775
1776 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub product_feedback_disabled: Option<bool>,
1779
1780 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1782 pub betas: Vec<String>,
1783
1784 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1786 pub capabilities: Vec<String>,
1787
1788 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1790 pub plugin_errors: Vec<PluginDiagnostic>,
1791
1792 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1794 pub plugin_warnings: Vec<PluginDiagnostic>,
1795
1796 #[serde(default, skip_serializing_if = "Option::is_none")]
1801 pub effort: Option<String>,
1802
1803 #[serde(default, skip_serializing_if = "Option::is_none")]
1808 pub cloud_session: Option<Value>,
1809
1810 #[serde(default, skip_serializing_if = "Option::is_none")]
1814 pub footer_indicator: Option<FooterIndicator>,
1815
1816 #[serde(default, skip_serializing_if = "Option::is_none")]
1820 pub worker_epoch: Option<u64>,
1821
1822 #[serde(default, skip_serializing_if = "Option::is_none")]
1826 pub powershell_path: Option<Option<String>>,
1827}
1828
1829#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1833pub struct FooterIndicator {
1834 pub text: String,
1837}
1838
1839#[derive(Debug, Clone, Serialize, Deserialize)]
1841pub struct StatusMessage {
1842 pub session_id: String,
1844 pub status: Option<StatusMessageStatus>,
1846 #[serde(skip_serializing_if = "Option::is_none")]
1848 pub uuid: Option<String>,
1849 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1851 pub permission_mode: Option<InitPermissionMode>,
1852 #[serde(skip_serializing_if = "Option::is_none")]
1853 pub compact_result: Option<String>,
1854 #[serde(skip_serializing_if = "Option::is_none")]
1855 pub compact_error: Option<String>,
1856}
1857
1858#[derive(Debug, Clone, Serialize, Deserialize)]
1860pub struct CompactBoundaryMessage {
1861 pub session_id: String,
1863 pub compact_metadata: CompactMetadata,
1865 #[serde(
1869 default,
1870 skip_serializing_if = "Option::is_none",
1871 alias = "content",
1872 alias = "text"
1873 )]
1874 pub summary: Option<String>,
1875 #[serde(
1879 default,
1880 skip_serializing_if = "Option::is_none",
1881 alias = "message_count"
1882 )]
1883 pub leaf_message_count: Option<u32>,
1884 #[serde(default, skip_serializing_if = "Option::is_none")]
1886 pub duration_ms: Option<u64>,
1887 #[serde(skip_serializing_if = "Option::is_none")]
1889 pub uuid: Option<String>,
1890 #[serde(skip_serializing_if = "Option::is_none")]
1892 pub logical_parent_uuid: Option<Option<String>>,
1893}
1894
1895#[derive(Debug, Clone, Serialize, Deserialize)]
1897pub struct CompactMetadata {
1898 pub pre_tokens: u64,
1900 pub trigger: CompactionTrigger,
1902 #[serde(default, skip_serializing_if = "Option::is_none")]
1903 pub post_tokens: Option<u64>,
1904 #[serde(default, skip_serializing_if = "Option::is_none")]
1905 pub cumulative_dropped_tokens: Option<u64>,
1906 #[serde(default, skip_serializing_if = "Option::is_none")]
1907 pub duration_ms: Option<u64>,
1908 #[serde(default, skip_serializing_if = "Option::is_none")]
1909 pub user_context: Option<String>,
1910 #[serde(default, skip_serializing_if = "Option::is_none")]
1911 pub messages_summarized: Option<u64>,
1912 #[serde(default, skip_serializing_if = "Option::is_none")]
1913 pub precomputed: Option<bool>,
1914 #[serde(default, skip_serializing_if = "Option::is_none")]
1915 pub pre_compact_discovered_tools: Option<Vec<String>>,
1916 #[serde(default, skip_serializing_if = "Option::is_none")]
1917 pub preserved_segment: Option<PreservedSegment>,
1918 #[serde(default, skip_serializing_if = "Option::is_none")]
1919 pub preserved_messages: Option<PreservedMessages>,
1920}
1921
1922#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1923pub struct PreservedSegment {
1924 pub head_uuid: String,
1925 pub anchor_uuid: String,
1926 pub tail_uuid: String,
1927}
1928
1929#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1930pub struct PreservedMessages {
1931 pub anchor_uuid: String,
1932 pub uuids: Vec<String>,
1933 #[serde(default, skip_serializing_if = "Option::is_none")]
1934 pub all_uuids: Option<Vec<String>>,
1935}
1936
1937#[derive(Debug, Clone, Serialize, Deserialize)]
1943pub struct TaskUsage {
1944 pub duration_ms: u64,
1946 pub tool_uses: u64,
1948 pub total_tokens: u64,
1950}
1951
1952#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1954pub enum TaskType {
1955 LocalAgent,
1957 LocalBash,
1959 LocalWorkflow,
1961 Unknown(String),
1963}
1964
1965impl TaskType {
1966 pub fn as_str(&self) -> &str {
1967 match self {
1968 Self::LocalAgent => "local_agent",
1969 Self::LocalBash => "local_bash",
1970 Self::LocalWorkflow => "local_workflow",
1971 Self::Unknown(s) => s.as_str(),
1972 }
1973 }
1974}
1975
1976impl fmt::Display for TaskType {
1977 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1978 f.write_str(self.as_str())
1979 }
1980}
1981
1982impl From<&str> for TaskType {
1983 fn from(s: &str) -> Self {
1984 match s {
1985 "local_agent" => Self::LocalAgent,
1986 "local_bash" => Self::LocalBash,
1987 "local_workflow" => Self::LocalWorkflow,
1988 other => Self::Unknown(other.to_string()),
1989 }
1990 }
1991}
1992
1993impl Serialize for TaskType {
1994 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1995 serializer.serialize_str(self.as_str())
1996 }
1997}
1998
1999impl<'de> Deserialize<'de> for TaskType {
2000 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2001 let s = String::deserialize(deserializer)?;
2002 Ok(Self::from(s.as_str()))
2003 }
2004}
2005
2006#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2008pub enum TaskStatus {
2009 Pending,
2010 Running,
2011 Completed,
2012 Failed,
2013 Killed,
2014 Paused,
2015 Stopped,
2016 Unknown(String),
2017}
2018
2019impl TaskStatus {
2020 pub fn as_str(&self) -> &str {
2021 match self {
2022 Self::Pending => "pending",
2023 Self::Running => "running",
2024 Self::Completed => "completed",
2025 Self::Failed => "failed",
2026 Self::Killed => "killed",
2027 Self::Paused => "paused",
2028 Self::Stopped => "stopped",
2029 Self::Unknown(s) => s.as_str(),
2030 }
2031 }
2032}
2033
2034impl fmt::Display for TaskStatus {
2035 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2036 f.write_str(self.as_str())
2037 }
2038}
2039
2040impl From<&str> for TaskStatus {
2041 fn from(s: &str) -> Self {
2042 match s {
2043 "pending" => Self::Pending,
2044 "running" => Self::Running,
2045 "completed" => Self::Completed,
2046 "failed" => Self::Failed,
2047 "killed" => Self::Killed,
2048 "paused" => Self::Paused,
2049 "stopped" => Self::Stopped,
2050 other => Self::Unknown(other.to_string()),
2051 }
2052 }
2053}
2054
2055impl Serialize for TaskStatus {
2056 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2057 serializer.serialize_str(self.as_str())
2058 }
2059}
2060
2061impl<'de> Deserialize<'de> for TaskStatus {
2062 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2063 let s = String::deserialize(deserializer)?;
2064 Ok(Self::from(s.as_str()))
2065 }
2066}
2067
2068#[derive(Debug, Clone, Serialize, Deserialize)]
2070pub struct TaskStartedMessage {
2071 pub session_id: String,
2072 pub task_id: String,
2073 #[serde(default, skip_serializing_if = "Option::is_none")]
2074 pub task_type: Option<TaskType>,
2075 #[serde(default, skip_serializing_if = "Option::is_none")]
2076 pub tool_use_id: Option<String>,
2077 pub description: String,
2078 #[serde(default, skip_serializing_if = "Option::is_none")]
2081 pub subagent_type: Option<String>,
2082 #[serde(default, skip_serializing_if = "Option::is_none")]
2088 pub is_backgrounded: Option<bool>,
2089 #[serde(default, skip_serializing_if = "Option::is_none")]
2093 pub spawn_depth: Option<u32>,
2094 #[serde(default, skip_serializing_if = "Option::is_none")]
2096 pub prompt: Option<String>,
2097 #[serde(default, skip_serializing_if = "Option::is_none")]
2098 pub workflow_name: Option<String>,
2099 #[serde(default, skip_serializing_if = "Option::is_none")]
2100 pub skip_transcript: Option<bool>,
2101 #[serde(default, skip_serializing_if = "Option::is_none")]
2106 pub ambient: Option<bool>,
2107 pub uuid: String,
2108}
2109
2110#[derive(Debug, Clone, Serialize, Deserialize)]
2114pub struct TaskUpdatedMessage {
2115 pub session_id: String,
2116 pub task_id: String,
2117 pub patch: TaskPatch,
2118 pub uuid: String,
2119}
2120
2121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2124pub struct TaskPatch {
2125 #[serde(default, skip_serializing_if = "Option::is_none")]
2126 pub status: Option<TaskStatus>,
2127 #[serde(default, skip_serializing_if = "Option::is_none")]
2130 pub end_time: Option<u64>,
2131 #[serde(default, skip_serializing_if = "Option::is_none")]
2132 pub description: Option<String>,
2133 #[serde(default, skip_serializing_if = "Option::is_none")]
2134 pub total_paused_ms: Option<u64>,
2135 #[serde(default, skip_serializing_if = "Option::is_none")]
2136 pub error: Option<String>,
2137 #[serde(default, skip_serializing_if = "Option::is_none")]
2138 pub is_backgrounded: Option<bool>,
2139}
2140
2141#[derive(Debug, Clone, Serialize, Deserialize)]
2144pub struct ThinkingTokensMessage {
2145 pub session_id: String,
2146 pub estimated_tokens: u64,
2148 pub estimated_tokens_delta: u64,
2150 #[serde(default, skip_serializing_if = "Option::is_none")]
2157 pub user_message_uuid: Option<String>,
2158 pub uuid: String,
2159}
2160
2161#[derive(Debug, Clone, Serialize, Deserialize)]
2164pub struct TaskProgressMessage {
2165 pub session_id: String,
2166 pub task_id: String,
2167 #[serde(default, skip_serializing_if = "Option::is_none")]
2168 pub tool_use_id: Option<String>,
2169 pub description: String,
2170 #[serde(default, skip_serializing_if = "Option::is_none")]
2171 pub last_tool_name: Option<String>,
2172 pub usage: TaskUsage,
2173 #[serde(default, skip_serializing_if = "Option::is_none")]
2175 pub subagent_type: Option<String>,
2176 #[serde(default, skip_serializing_if = "Option::is_none")]
2177 pub summary: Option<String>,
2178 pub uuid: String,
2179}
2180
2181#[derive(Debug, Clone, Serialize, Deserialize)]
2184pub struct TaskNotificationMessage {
2185 pub session_id: String,
2186 pub task_id: String,
2187 pub status: TaskStatus,
2188 pub summary: String,
2189 pub output_file: Option<String>,
2190 #[serde(skip_serializing_if = "Option::is_none")]
2191 pub tool_use_id: Option<String>,
2192 #[serde(skip_serializing_if = "Option::is_none")]
2193 pub usage: Option<TaskUsage>,
2194 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2200 pub resource_links: Vec<ResourceLink>,
2201 #[serde(default, skip_serializing_if = "Option::is_none")]
2202 pub skip_transcript: Option<bool>,
2203 #[serde(default, skip_serializing_if = "Option::is_none")]
2206 pub ambient: Option<bool>,
2207 #[serde(skip_serializing_if = "Option::is_none")]
2208 pub uuid: Option<String>,
2209}
2210
2211#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2213pub enum AssistantErrorKind {
2214 AuthenticationFailed,
2215 OauthOrgNotAllowed,
2216 AccountOnHold,
2217 BillingError,
2218 RateLimit,
2219 Overloaded,
2220 InvalidRequest,
2221 ModelNotFound,
2222 ServerError,
2223 UnknownError,
2224 MaxOutputTokens,
2225 Unknown(String),
2226}
2227
2228impl AssistantErrorKind {
2229 pub fn as_str(&self) -> &str {
2230 match self {
2231 Self::AuthenticationFailed => "authentication_failed",
2232 Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
2233 Self::AccountOnHold => "account_on_hold",
2234 Self::BillingError => "billing_error",
2235 Self::RateLimit => "rate_limit",
2236 Self::Overloaded => "overloaded",
2237 Self::InvalidRequest => "invalid_request",
2238 Self::ModelNotFound => "model_not_found",
2239 Self::ServerError => "server_error",
2240 Self::UnknownError => "unknown",
2241 Self::MaxOutputTokens => "max_output_tokens",
2242 Self::Unknown(s) => s.as_str(),
2243 }
2244 }
2245}
2246
2247impl fmt::Display for AssistantErrorKind {
2248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2249 f.write_str(self.as_str())
2250 }
2251}
2252
2253impl From<&str> for AssistantErrorKind {
2254 fn from(s: &str) -> Self {
2255 match s {
2256 "authentication_failed" => Self::AuthenticationFailed,
2257 "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
2258 "account_on_hold" => Self::AccountOnHold,
2259 "billing_error" => Self::BillingError,
2260 "rate_limit" => Self::RateLimit,
2261 "overloaded" => Self::Overloaded,
2262 "invalid_request" => Self::InvalidRequest,
2263 "model_not_found" => Self::ModelNotFound,
2264 "server_error" => Self::ServerError,
2265 "unknown" => Self::UnknownError,
2266 "max_output_tokens" => Self::MaxOutputTokens,
2267 other => Self::Unknown(other.to_string()),
2268 }
2269 }
2270}
2271
2272impl Serialize for AssistantErrorKind {
2273 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2274 serializer.serialize_str(self.as_str())
2275 }
2276}
2277
2278impl<'de> Deserialize<'de> for AssistantErrorKind {
2279 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2280 let s = String::deserialize(deserializer)?;
2281 Ok(Self::from(s.as_str()))
2282 }
2283}
2284
2285#[derive(Debug, Clone, Serialize, Deserialize)]
2292pub struct CodeChangePublishedMessage {
2293 pub provider: String,
2297 pub url: String,
2299 pub repo: String,
2302 pub identifier: String,
2304 #[serde(default, skip_serializing_if = "Option::is_none")]
2313 pub action: Option<String>,
2314 #[serde(default, skip_serializing_if = "Option::is_none")]
2319 pub branch: Option<String>,
2320 pub uuid: String,
2321 pub session_id: String,
2322}
2323
2324#[derive(Debug, Clone, Serialize, Deserialize)]
2329pub struct VcsStateChangedMessage {
2330 pub kind: VcsMutationKind,
2333 pub cwd: String,
2336 #[serde(default, skip_serializing_if = "Option::is_none")]
2341 pub branch: Option<String>,
2342 pub uuid: String,
2343 pub session_id: String,
2344}
2345
2346#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2348pub enum VcsMutationKind {
2349 Commit,
2350 Push,
2351 Merge,
2352 Rebase,
2353 Unknown(String),
2355}
2356
2357impl VcsMutationKind {
2358 pub fn as_str(&self) -> &str {
2359 match self {
2360 Self::Commit => "commit",
2361 Self::Push => "push",
2362 Self::Merge => "merge",
2363 Self::Rebase => "rebase",
2364 Self::Unknown(s) => s.as_str(),
2365 }
2366 }
2367}
2368
2369impl fmt::Display for VcsMutationKind {
2370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2371 f.write_str(self.as_str())
2372 }
2373}
2374
2375impl From<&str> for VcsMutationKind {
2376 fn from(s: &str) -> Self {
2377 match s {
2378 "commit" => Self::Commit,
2379 "push" => Self::Push,
2380 "merge" => Self::Merge,
2381 "rebase" => Self::Rebase,
2382 other => Self::Unknown(other.to_string()),
2383 }
2384 }
2385}
2386
2387impl Serialize for VcsMutationKind {
2388 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2389 serializer.serialize_str(self.as_str())
2390 }
2391}
2392
2393impl<'de> Deserialize<'de> for VcsMutationKind {
2394 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2395 let s = String::deserialize(deserializer)?;
2396 Ok(Self::from(s.as_str()))
2397 }
2398}
2399
2400#[derive(Debug, Clone, Serialize, Deserialize)]
2402pub struct FeedbackDraftQueuedMessage {
2403 pub draft_id: String,
2404 pub draft_type: String,
2405 pub title: String,
2406 pub details_preview: String,
2407 #[serde(default, skip_serializing_if = "Option::is_none")]
2408 pub uuid: Option<String>,
2409 #[serde(default, skip_serializing_if = "Option::is_none")]
2410 pub session_id: Option<String>,
2411 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2412 pub extra: serde_json::Map<String, Value>,
2413}
2414
2415#[derive(Debug, Clone, Serialize, Deserialize)]
2423pub struct CloudSessionDeltaMessage {
2424 pub seq: u64,
2428 pub changed: Vec<String>,
2432 pub cloud_session: Value,
2436 pub uuid: String,
2437 pub session_id: String,
2438 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2439 pub extra: serde_json::Map<String, Value>,
2440}
2441
2442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2445pub struct BatchToolUse {
2446 pub id: String,
2447 pub name: String,
2448}
2449
2450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2455pub struct ContextUsage {
2456 pub model: String,
2458 pub total_tokens: u64,
2461 pub raw_max_tokens: u64,
2464 pub percentage: u64,
2466 #[serde(default, skip_serializing_if = "Option::is_none")]
2468 pub over_limit: Option<ContextOverLimit>,
2469 #[serde(default)]
2471 pub categories: Vec<ContextCategory>,
2472 #[serde(default)]
2474 pub mcp_tools: Vec<ContextMcpTool>,
2475 #[serde(default)]
2477 pub memory_files: Vec<ContextMemoryFile>,
2478 #[serde(default)]
2480 pub agents: Vec<ContextAgent>,
2481 #[serde(default, skip_serializing_if = "Option::is_none")]
2483 pub skills: Option<Vec<ContextSkill>>,
2484}
2485
2486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2488pub struct ContextOverLimit {
2489 pub tokens_over: u64,
2490 pub kind: String,
2493}
2494
2495#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2497pub struct ContextCategory {
2498 pub name: String,
2501 pub tokens: u64,
2502 pub kind: String,
2506}
2507
2508#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2510pub struct ContextMcpTool {
2511 pub name: String,
2513 pub server_name: String,
2514 pub tokens: u64,
2515}
2516
2517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2519pub struct ContextMemoryFile {
2520 pub path: String,
2521 #[serde(rename = "type")]
2523 pub file_type: String,
2524 pub tokens: u64,
2525}
2526
2527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2529pub struct ContextAgent {
2530 pub agent_type: String,
2531 pub source: String,
2533 pub tokens: u64,
2534}
2535
2536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2538pub struct ContextSkill {
2539 pub name: String,
2540 pub source: String,
2542 #[serde(default, skip_serializing_if = "Option::is_none")]
2543 pub plugin_name: Option<String>,
2544 pub tokens: u64,
2545}
2546
2547#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2549pub struct ToolUseMeta {
2550 pub id: String,
2551 pub display_name: String,
2552 #[serde(default, skip_serializing_if = "Option::is_none")]
2553 pub server_display_name: Option<String>,
2554 #[serde(default, skip_serializing_if = "Option::is_none")]
2555 pub icon_url: Option<String>,
2556}
2557
2558#[derive(Debug, Clone, Serialize, Deserialize)]
2560pub struct AssistantMessage {
2561 pub message: AssistantMessageContent,
2562 #[serde(alias = "sessionId")]
2563 pub session_id: String,
2564 #[serde(skip_serializing_if = "Option::is_none")]
2565 pub uuid: Option<String>,
2566 #[serde(skip_serializing_if = "Option::is_none")]
2567 pub parent_tool_use_id: Option<String>,
2568 #[serde(skip_serializing_if = "Option::is_none")]
2570 pub request_id: Option<String>,
2571 #[serde(default, skip_serializing_if = "Option::is_none")]
2577 pub user_message_uuid: Option<String>,
2578 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2584 pub user_message_uuids: Vec<String>,
2585 #[serde(skip_serializing_if = "Option::is_none")]
2588 pub subagent_type: Option<String>,
2589 #[serde(skip_serializing_if = "Option::is_none")]
2591 pub task_description: Option<String>,
2592 #[serde(skip_serializing_if = "Option::is_none")]
2593 pub error: Option<AssistantErrorKind>,
2594 #[serde(default, skip_serializing_if = "Option::is_none")]
2598 pub aborted: Option<bool>,
2599 #[serde(default, skip_serializing_if = "Option::is_none")]
2604 pub resumed_from_incomplete_thinking: Option<bool>,
2605 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2606 pub supersedes: Vec<String>,
2607 #[serde(skip_serializing_if = "Option::is_none")]
2608 pub timestamp: Option<String>,
2609 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2610 pub tool_use_meta: Vec<ToolUseMeta>,
2611 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2616 pub batch_tool_uses: Vec<BatchToolUse>,
2617 #[serde(default, skip_serializing_if = "Option::is_none")]
2623 pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
2624 #[serde(default, skip_serializing_if = "Option::is_none")]
2630 pub local_command_source: Option<String>,
2631 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2640 pub narration_block_indexes: Vec<usize>,
2641 #[serde(default, skip_serializing_if = "Option::is_none")]
2645 pub context_usage: Option<ContextUsage>,
2646 #[serde(default, skip_serializing_if = "Option::is_none")]
2647 pub is_meta: Option<bool>,
2648 #[serde(default, skip_serializing_if = "Option::is_none")]
2649 pub is_virtual: Option<bool>,
2650 #[serde(default, skip_serializing_if = "Option::is_none")]
2651 pub is_api_error_message: Option<bool>,
2652 #[serde(skip_serializing_if = "Option::is_none")]
2653 pub api_error_status: Option<u16>,
2654 #[serde(skip_serializing_if = "Option::is_none")]
2655 pub api_error: Option<String>,
2656 #[serde(skip_serializing_if = "Option::is_none")]
2657 pub error_details: Option<String>,
2658 #[serde(skip_serializing_if = "Option::is_none")]
2659 pub advisor_model: Option<String>,
2660 #[serde(skip_serializing_if = "Option::is_none")]
2661 pub attribution_agent: Option<String>,
2662 #[serde(skip_serializing_if = "Option::is_none")]
2663 pub attribution_skill: Option<String>,
2664 #[serde(skip_serializing_if = "Option::is_none")]
2665 pub attribution_plugin: Option<String>,
2666 #[serde(skip_serializing_if = "Option::is_none")]
2667 pub attribution_mcp_server: Option<String>,
2668 #[serde(skip_serializing_if = "Option::is_none")]
2669 pub attribution_mcp_tool: Option<String>,
2670}
2671
2672#[derive(Debug, Clone, Serialize, Deserialize)]
2674pub struct AssistantMessageContent {
2675 pub id: String,
2676 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2678 pub message_type: Option<String>,
2679 pub role: MessageRole,
2680 pub model: String,
2681 pub content: Vec<ContentBlock>,
2682 #[serde(skip_serializing_if = "Option::is_none")]
2683 pub stop_reason: Option<StopReason>,
2684 #[serde(skip_serializing_if = "Option::is_none")]
2685 pub stop_sequence: Option<String>,
2686 #[serde(skip_serializing_if = "Option::is_none")]
2687 pub usage: Option<AssistantUsage>,
2688 #[serde(skip_serializing_if = "Option::is_none")]
2690 pub stop_details: Option<Value>,
2691 #[serde(skip_serializing_if = "Option::is_none")]
2693 pub context_management: Option<Value>,
2694}
2695
2696#[derive(Debug, Clone, Serialize, Deserialize)]
2698pub struct AssistantUsage {
2699 #[serde(default)]
2701 pub input_tokens: u32,
2702
2703 #[serde(default)]
2705 pub output_tokens: u32,
2706
2707 #[serde(default)]
2709 pub cache_creation_input_tokens: u32,
2710
2711 #[serde(default)]
2713 pub cache_read_input_tokens: u32,
2714
2715 #[serde(skip_serializing_if = "Option::is_none")]
2717 pub service_tier: Option<String>,
2718
2719 #[serde(skip_serializing_if = "Option::is_none")]
2721 pub cache_creation: Option<CacheCreationDetails>,
2722
2723 #[serde(skip_serializing_if = "Option::is_none")]
2725 pub inference_geo: Option<String>,
2726}
2727
2728#[derive(Debug, Clone, Serialize, Deserialize)]
2730pub struct CacheCreationDetails {
2731 #[serde(default)]
2733 pub ephemeral_1h_input_tokens: u32,
2734
2735 #[serde(default)]
2737 pub ephemeral_5m_input_tokens: u32,
2738}
2739
2740#[cfg(test)]
2741mod tests {
2742 use crate::io::ClaudeOutput;
2743
2744 #[test]
2745 fn test_subagent_usage_rollup_accumulates_task_results() {
2746 use super::SubagentUsageRollup;
2747
2748 let mut rollup = SubagentUsageRollup::default();
2749
2750 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}}"#;
2751 let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2752 assert!(rollup.observe(&output));
2753 assert_eq!(rollup.subagent_tokens, 10201);
2754 assert_eq!(rollup.agent_count, 1);
2755 assert_eq!(rollup.tool_uses, 3);
2756 assert_eq!(rollup.duration_ms, 1853);
2757
2758 assert!(!rollup.observe(&output));
2760 assert_eq!(rollup.agent_count, 1);
2761 assert_eq!(rollup.subagent_tokens, 10201);
2762
2763 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}}"#;
2765 let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2766 assert!(rollup.observe(&output));
2767 assert_eq!(rollup.agent_count, 2);
2768 assert_eq!(rollup.subagent_tokens, 10701);
2769 }
2770
2771 #[test]
2772 fn test_subagent_usage_rollup_ignores_non_task_results() {
2773 use super::SubagentUsageRollup;
2774
2775 let mut rollup = SubagentUsageRollup::default();
2776
2777 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}}"#;
2780 let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2781 assert!(!rollup.observe(&output));
2782
2783 let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2785 let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2786 assert!(!rollup.observe(&output));
2787
2788 let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2790 let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2791 assert!(!rollup.observe(&output));
2792
2793 assert_eq!(rollup, SubagentUsageRollup::default());
2794 }
2795
2796 #[test]
2797 fn test_subagent_usage_rollup_over_captured_session() {
2798 use super::SubagentUsageRollup;
2799
2800 let mut rollup = SubagentUsageRollup::default();
2801 let fixture =
2802 include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2803 for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2804 if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2805 rollup.observe(&output);
2806 }
2807 }
2808 assert_eq!(rollup.agent_count, 1);
2809 assert_eq!(rollup.subagent_tokens, 10201);
2810 }
2811
2812 #[test]
2813 fn test_system_message_init() {
2814 let json = r#"{
2815 "type": "system",
2816 "subtype": "init",
2817 "session_id": "test-session-123",
2818 "cwd": "/home/user/project",
2819 "model": "claude-sonnet-4",
2820 "tools": ["Bash", "Read", "Write"],
2821 "mcp_servers": [],
2822 "slash_commands": ["compact", "cost", "review"],
2823 "agents": ["Bash", "Explore", "Plan"],
2824 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2825 "skills": [],
2826 "claude_code_version": "2.1.15",
2827 "apiKeySource": "none",
2828 "output_style": "default",
2829 "permissionMode": "default"
2830 }"#;
2831
2832 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2833 if let ClaudeOutput::System(sys) = output {
2834 assert!(sys.is_init());
2835 assert!(!sys.is_status());
2836 assert!(!sys.is_compact_boundary());
2837
2838 let init = sys.as_init().expect("Should parse as init");
2839 assert_eq!(init.session_id, "test-session-123");
2840 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2841 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2842 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2843 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2844 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2845 assert_eq!(init.plugins.len(), 1);
2846 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2847 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2848 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2849 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2850 assert_eq!(
2851 init.permission_mode,
2852 Some(super::InitPermissionMode::Default)
2853 );
2854 } else {
2855 panic!("Expected System message");
2856 }
2857 }
2858
2859 #[test]
2860 fn test_system_message_init_from_real_capture() {
2861 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2862 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2863 if let ClaudeOutput::System(sys) = output {
2864 let init = sys.as_init().expect("Should parse real init capture");
2865 assert_eq!(init.slash_commands.len(), 8);
2866 assert!(init.slash_commands.contains(&"compact".to_string()));
2867 assert!(init.slash_commands.contains(&"review".to_string()));
2868 assert_eq!(init.agents.len(), 5);
2869 assert!(init.agents.contains(&"Bash".to_string()));
2870 assert!(init.agents.contains(&"Explore".to_string()));
2871 assert_eq!(init.plugins.len(), 1);
2872 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2873 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2874 } else {
2875 panic!("Expected System message");
2876 }
2877 }
2878
2879 #[test]
2880 fn test_system_message_status() {
2881 let json = r#"{
2882 "type": "system",
2883 "subtype": "status",
2884 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2885 "status": "compacting",
2886 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2887 }"#;
2888
2889 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2890 if let ClaudeOutput::System(sys) = output {
2891 assert!(sys.is_status());
2892 assert!(!sys.is_init());
2893
2894 let status = sys.as_status().expect("Should parse as status");
2895 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2896 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
2897 assert_eq!(
2898 status.uuid,
2899 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2900 );
2901 } else {
2902 panic!("Expected System message");
2903 }
2904 }
2905
2906 #[test]
2907 fn test_system_message_status_null() {
2908 let json = r#"{
2909 "type": "system",
2910 "subtype": "status",
2911 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2912 "status": null,
2913 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2914 }"#;
2915
2916 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2917 if let ClaudeOutput::System(sys) = output {
2918 let status = sys.as_status().expect("Should parse as status");
2919 assert_eq!(status.status, None);
2920 } else {
2921 panic!("Expected System message");
2922 }
2923 }
2924
2925 #[test]
2926 fn test_system_message_task_started() {
2927 let json = r#"{
2928 "type": "system",
2929 "subtype": "task_started",
2930 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2931 "task_id": "b6daf3f",
2932 "task_type": "local_bash",
2933 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
2934 "description": "Wait for CI on PR #12",
2935 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
2936 }"#;
2937
2938 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2939 if let ClaudeOutput::System(sys) = output {
2940 assert!(sys.is_task_started());
2941 assert!(!sys.is_task_progress());
2942 assert!(!sys.is_task_notification());
2943
2944 let task = sys.as_task_started().expect("Should parse as task_started");
2945 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
2946 assert_eq!(task.task_id, "b6daf3f");
2947 assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
2948 assert_eq!(
2949 task.tool_use_id.as_deref(),
2950 Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
2951 );
2952 assert_eq!(task.description, "Wait for CI on PR #12");
2953 } else {
2954 panic!("Expected System message");
2955 }
2956 }
2957
2958 #[test]
2959 fn test_system_message_task_started_agent() {
2960 let json = r#"{
2961 "type": "system",
2962 "subtype": "task_started",
2963 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2964 "task_id": "a4a7e0906e5fc64cc",
2965 "task_type": "local_agent",
2966 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2967 "description": "Explore Scene/ArrayScene duplication",
2968 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2969 }"#;
2970
2971 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2972 if let ClaudeOutput::System(sys) = output {
2973 let task = sys.as_task_started().expect("Should parse as task_started");
2974 assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
2975 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
2976 } else {
2977 panic!("Expected System message");
2978 }
2979 }
2980
2981 #[test]
2982 fn test_system_message_task_progress() {
2983 let json = r#"{
2984 "type": "system",
2985 "subtype": "task_progress",
2986 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2987 "task_id": "a4a7e0906e5fc64cc",
2988 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2989 "description": "Reading src/jplephem/chebyshev.rs",
2990 "last_tool_name": "Read",
2991 "usage": {
2992 "duration_ms": 13996,
2993 "tool_uses": 9,
2994 "total_tokens": 38779
2995 },
2996 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2997 }"#;
2998
2999 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3000 if let ClaudeOutput::System(sys) = output {
3001 assert!(sys.is_task_progress());
3002 assert!(!sys.is_task_started());
3003
3004 let progress = sys
3005 .as_task_progress()
3006 .expect("Should parse as task_progress");
3007 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
3008 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
3009 assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
3010 assert_eq!(progress.usage.duration_ms, 13996);
3011 assert_eq!(progress.usage.tool_uses, 9);
3012 assert_eq!(progress.usage.total_tokens, 38779);
3013 } else {
3014 panic!("Expected System message");
3015 }
3016 }
3017
3018 #[test]
3019 fn test_system_message_task_notification_completed() {
3020 let json = r#"{
3021 "type": "system",
3022 "subtype": "task_notification",
3023 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3024 "task_id": "a0ba761e9dc9c316f",
3025 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
3026 "status": "completed",
3027 "summary": "Agent \"Write Hipparcos data source doc\" completed",
3028 "output_file": "",
3029 "usage": {
3030 "duration_ms": 172300,
3031 "tool_uses": 11,
3032 "total_tokens": 42005
3033 },
3034 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
3035 }"#;
3036
3037 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3038 if let ClaudeOutput::System(sys) = output {
3039 assert!(sys.is_task_notification());
3040
3041 let notif = sys
3042 .as_task_notification()
3043 .expect("Should parse as task_notification");
3044 assert_eq!(notif.status, super::TaskStatus::Completed);
3045 assert_eq!(
3046 notif.summary,
3047 "Agent \"Write Hipparcos data source doc\" completed"
3048 );
3049 assert_eq!(notif.output_file, Some("".to_string()));
3050 assert_eq!(
3051 notif.tool_use_id,
3052 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
3053 );
3054 let usage = notif.usage.expect("Should have usage");
3055 assert_eq!(usage.duration_ms, 172300);
3056 assert_eq!(usage.tool_uses, 11);
3057 assert_eq!(usage.total_tokens, 42005);
3058 } else {
3059 panic!("Expected System message");
3060 }
3061 }
3062
3063 #[test]
3064 fn test_system_message_task_notification_failed_no_usage() {
3065 let json = r#"{
3066 "type": "system",
3067 "subtype": "task_notification",
3068 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
3069 "task_id": "b98f6a3",
3070 "status": "failed",
3071 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
3072 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
3073 }"#;
3074
3075 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3076 if let ClaudeOutput::System(sys) = output {
3077 let notif = sys
3078 .as_task_notification()
3079 .expect("Should parse as task_notification");
3080 assert_eq!(notif.status, super::TaskStatus::Failed);
3081 assert!(notif.tool_use_id.is_none());
3082 assert!(notif.usage.is_none());
3083 assert_eq!(
3084 notif.output_file,
3085 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
3086 );
3087 } else {
3088 panic!("Expected System message");
3089 }
3090 }
3091
3092 #[test]
3098 fn test_task_messages_roundtrip_through_value() {
3099 let cases = [
3100 r#"{"type":"system","subtype":"task_started","session_id":"s1",
3101 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
3102 "description":"Sleep 3s","uuid":"u1"}"#,
3103 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
3104 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
3105 "last_tool_name":"Bash",
3106 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3107 "uuid":"u2"}"#,
3108 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
3109 "task_id":"t1","tool_use_id":"tu1","status":"completed",
3110 "summary":"done","output_file":"",
3111 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3112 "uuid":"u3"}"#,
3113 ];
3114
3115 for json in cases {
3116 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3117 let value = serde_json::to_value(&output).unwrap();
3118 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
3119
3120 let ClaudeOutput::System(sys) = reparsed else {
3121 panic!("Expected System variant after round-trip");
3122 };
3123
3124 match sys.subtype {
3125 super::SystemSubtype::TaskStarted => {
3126 assert!(
3127 sys.as_task_started().is_some(),
3128 "as_task_started failed after round-trip"
3129 );
3130 }
3131 super::SystemSubtype::TaskProgress => {
3132 assert!(
3133 sys.as_task_progress().is_some(),
3134 "as_task_progress failed after round-trip"
3135 );
3136 }
3137 super::SystemSubtype::TaskNotification => {
3138 assert!(
3139 sys.as_task_notification().is_some(),
3140 "as_task_notification failed after round-trip"
3141 );
3142 }
3143 other => panic!("unexpected subtype after round-trip: {other:?}"),
3144 }
3145 }
3146 }
3147
3148 #[test]
3149 fn test_system_message_compact_boundary() {
3150 let json = r#"{
3151 "type": "system",
3152 "subtype": "compact_boundary",
3153 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3154 "compact_metadata": {
3155 "pre_tokens": 155285,
3156 "trigger": "auto"
3157 },
3158 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
3159 }"#;
3160
3161 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3162 if let ClaudeOutput::System(sys) = output {
3163 assert!(sys.is_compact_boundary());
3164 assert!(!sys.is_init());
3165 assert!(!sys.is_status());
3166
3167 let compact = sys
3168 .as_compact_boundary()
3169 .expect("Should parse as compact_boundary");
3170 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3171 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
3172 assert_eq!(
3173 compact.compact_metadata.trigger,
3174 super::CompactionTrigger::Auto
3175 );
3176 assert!(compact.summary.is_none());
3178 assert!(compact.leaf_message_count.is_none());
3179 assert!(compact.duration_ms.is_none());
3180 } else {
3181 panic!("Expected System message");
3182 }
3183 }
3184
3185 #[test]
3186 fn test_compact_boundary_with_summary_stats() {
3187 let json = r#"{
3189 "type": "system",
3190 "subtype": "compact_boundary",
3191 "session_id": "s1",
3192 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
3193 "summary": "Summarized the earlier exploration.",
3194 "leaf_message_count": 42,
3195 "duration_ms": 1234,
3196 "uuid": "u1"
3197 }"#;
3198 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3199 let ClaudeOutput::System(sys) = output else {
3200 panic!("Expected System message");
3201 };
3202 let compact = sys.as_compact_boundary().expect("compact_boundary");
3203 assert_eq!(
3204 compact.summary.as_deref(),
3205 Some("Summarized the earlier exploration.")
3206 );
3207 assert_eq!(compact.leaf_message_count, Some(42));
3208 assert_eq!(compact.duration_ms, Some(1234));
3209
3210 let json_alt = r#"{
3213 "type": "system",
3214 "subtype": "compact_boundary",
3215 "session_id": "s2",
3216 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
3217 "content": "alt-key summary",
3218 "message_count": 7
3219 }"#;
3220 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
3221 let ClaudeOutput::System(sys) = output else {
3222 panic!("Expected System message");
3223 };
3224 let compact = sys.as_compact_boundary().expect("compact_boundary");
3225 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
3226 assert_eq!(compact.leaf_message_count, Some(7));
3227 }
3228
3229 #[test]
3230 fn test_init_message_with_new_fields() {
3231 let json = r#"{
3232 "type": "system",
3233 "subtype": "init",
3234 "session_id": "test-session",
3235 "cwd": "/home/user",
3236 "model": "claude-opus-4-7",
3237 "tools": ["Bash"],
3238 "mcp_servers": [],
3239 "permissionMode": "default",
3240 "apiKeySource": "none",
3241 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
3242 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
3243 "fast_mode_state": "off",
3244 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
3245 "claude_code_version": "2.1.117"
3246 }"#;
3247
3248 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3249 if let ClaudeOutput::System(sys) = output {
3250 let init = sys.as_init().expect("Should parse as init");
3251 assert_eq!(
3252 init.uuid.as_deref(),
3253 Some("44841a0d-182d-493a-86b5-79800d3d9665")
3254 );
3255 assert!(init.memory_paths.is_some());
3256 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
3257 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
3258 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
3259 } else {
3260 panic!("Expected System message");
3261 }
3262 }
3263
3264 #[test]
3265 fn test_assistant_message_with_new_fields() {
3266 let json = r#"{
3267 "type": "assistant",
3268 "message": {
3269 "id": "msg_1",
3270 "type": "message",
3271 "role": "assistant",
3272 "model": "claude-opus-4-7",
3273 "content": [{"type": "text", "text": "Hello"}],
3274 "stop_reason": "end_turn",
3275 "stop_details": null,
3276 "context_management": null,
3277 "usage": {
3278 "input_tokens": 100,
3279 "output_tokens": 10,
3280 "cache_creation_input_tokens": 50,
3281 "cache_read_input_tokens": 0,
3282 "service_tier": "standard",
3283 "inference_geo": "not_available"
3284 }
3285 },
3286 "session_id": "abc",
3287 "uuid": "msg-uuid-123"
3288 }"#;
3289
3290 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3291 if let ClaudeOutput::Assistant(asst) = output {
3292 assert_eq!(asst.message.stop_details, None);
3293 assert_eq!(asst.message.context_management, None);
3294 let usage = asst.message.usage.unwrap();
3295 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
3296 } else {
3297 panic!("Expected Assistant message");
3298 }
3299 }
3300
3301 #[test]
3302 fn test_user_message_with_new_fields() {
3303 let json = r#"{
3304 "type": "user",
3305 "message": {
3306 "role": "user",
3307 "content": [{"type": "text", "text": "Hello"}]
3308 },
3309 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3310 "parent_tool_use_id": "toolu_123",
3311 "uuid": "user-msg-456"
3312 }"#;
3313
3314 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3315 if let ClaudeOutput::User(user) = output {
3316 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
3317 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
3318 } else {
3319 panic!("Expected User message");
3320 }
3321 }
3322
3323 #[test]
3329 fn test_user_message_preserves_tool_use_result_and_timestamp() {
3330 let json = r#"{
3331 "type":"user",
3332 "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"}]},
3333 "parent_tool_use_id":null,
3334 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3335 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
3336 "timestamp":"2026-05-12T23:12:04.121Z",
3337 "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"}}
3338 }"#;
3339
3340 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3341 let user = match output {
3342 ClaudeOutput::User(u) => u,
3343 other => panic!("Expected User message, got {:?}", other.message_type()),
3344 };
3345
3346 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
3347 let raw = user
3348 .tool_use_result
3349 .as_ref()
3350 .expect("tool_use_result must be captured");
3351 assert_eq!(raw["answers"]["Color"], "Blue");
3352 assert_eq!(raw["questions"][0]["header"], "Color");
3353
3354 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
3358 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
3359 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
3360 assert_eq!(
3361 reser["tool_use_result"]["questions"][0]["question"],
3362 "Which color do you prefer?"
3363 );
3364
3365 let typed: crate::AskUserQuestionInput = user
3368 .tool_use_result_as::<crate::AskUserQuestionInput>()
3369 .expect("tool_use_result present")
3370 .expect("AskUserQuestionInput parses");
3371 assert_eq!(typed.questions.len(), 1);
3372 assert_eq!(typed.questions[0].header, "Color");
3373 let answers = typed.answers.expect("answers populated");
3374 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3375 }
3376
3377 #[test]
3380 fn test_user_message_without_tool_use_result_omits_field() {
3381 let json = r#"{
3382 "type":"user",
3383 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3384 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3385 }"#;
3386
3387 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3388 let user = match output {
3389 ClaudeOutput::User(u) => u,
3390 _ => panic!("Expected User message"),
3391 };
3392 assert!(user.tool_use_result.is_none());
3393 assert!(user.timestamp.is_none());
3394
3395 let reser = serde_json::to_value(&user).unwrap();
3396 assert!(reser.get("tool_use_result").is_none());
3397 assert!(reser.get("timestamp").is_none());
3398 }
3399
3400 #[test]
3404 fn test_subagent_result_exposes_token_accounting() {
3405 let json = r#"{
3406 "type":"user",
3407 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3408 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3409 "tool_use_result":{
3410 "status":"completed",
3411 "prompt":"Count the .rs files.",
3412 "agentId":"ac4f0276e9d4b6232",
3413 "agentType":"Explore",
3414 "content":[{"type":"text","text":"21"}],
3415 "resolvedModel":"claude-haiku-4-5-20251001",
3416 "totalDurationMs":6869,
3417 "totalTokens":7834,
3418 "totalToolUseCount":1,
3419 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3420 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3421 }
3422 }"#;
3423
3424 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3425 let user = match output {
3426 ClaudeOutput::User(u) => u,
3427 _ => panic!("Expected User message"),
3428 };
3429
3430 let result = user.subagent_result().expect("subagent result parses");
3431 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3432 assert_eq!(
3433 result.resolved_model.as_deref(),
3434 Some("claude-haiku-4-5-20251001")
3435 );
3436 assert_eq!(result.total_tokens, Some(7834));
3437 assert_eq!(result.total_duration_ms, Some(6869));
3438 assert_eq!(result.total_tool_use_count, Some(1));
3439
3440 let usage = result.usage.expect("nested usage present");
3441 assert_eq!(usage.input_tokens, 6);
3442 assert_eq!(usage.cache_read_input_tokens, 7699);
3443
3444 let stats = result.tool_stats.expect("toolStats present");
3445 assert_eq!(stats.bash_count, 1);
3446 }
3447
3448 #[test]
3452 fn test_subagent_result_absent_for_non_task_result() {
3453 let json = r#"{
3454 "type":"user",
3455 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3456 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3457 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3458 }"#;
3459
3460 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3461 let user = match output {
3462 ClaudeOutput::User(u) => u,
3463 _ => panic!("Expected User message"),
3464 };
3465
3466 let result = user.subagent_result().expect("lenient parse");
3467 assert_eq!(result.total_tokens, None);
3468 assert_eq!(result.agent_type, None);
3469 }
3470
3471 #[test]
3472 fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3473 use serde_json::Value;
3474
3475 let raw: Value = serde_json::from_str(
3476 r#"{
3477 "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3478 "fast_mode_state":"off",
3479 "fast_mode_disabled_reason":"not_first_party",
3480 "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3481 }"#,
3482 )
3483 .unwrap();
3484 crate::io::assert_fully_wrapped(&raw);
3485
3486 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3487 let ClaudeOutput::System(sys) = output else {
3488 panic!("expected System");
3489 };
3490 let init = sys.as_init().expect("parses as init");
3491 assert_eq!(
3492 init.fast_mode_disabled_reason,
3493 Some(crate::FastModeDisabledReason::NotFirstParty)
3494 );
3495 let errs = init.mcp_server_errors.unwrap();
3496 assert_eq!(errs.len(), 1);
3497 assert_eq!(errs[0].name, "broken");
3498 assert_eq!(errs[0].error_type, "invalid_config");
3499 }
3500
3501 #[test]
3502 fn test_code_change_published_fully_wrapped() {
3503 use super::{KnownSystemEvent, SystemSubtype};
3504 use serde_json::Value;
3505
3506 let raw: Value = serde_json::from_str(
3507 r#"{
3508 "type":"system","subtype":"code_change_published",
3509 "provider":"github","url":"https://github.com/owner/repo/pull/42",
3510 "repo":"owner/repo","identifier":"42",
3511 "uuid":"u1","session_id":"s1"
3512 }"#,
3513 )
3514 .unwrap();
3515 crate::io::assert_fully_wrapped(&raw);
3516
3517 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3518 let ClaudeOutput::System(sys) = output else {
3519 panic!("expected System");
3520 };
3521 assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3522 let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3523 panic!("expected CodeChangePublished event");
3524 };
3525 assert_eq!(msg.provider, "github");
3526 assert_eq!(msg.repo, "owner/repo");
3527 assert_eq!(msg.identifier, "42");
3528
3529 assert!(sys.is_code_change_published());
3530 assert!(!sys.is_vcs_state_changed());
3531 let direct = sys.as_code_change_published().expect("direct accessor");
3532 assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3533 assert!(sys.as_vcs_state_changed().is_none());
3534 }
3535
3536 #[test]
3537 fn test_feedback_draft_queued_fully_wrapped() {
3538 use super::{KnownSystemEvent, SystemSubtype};
3539 use serde_json::Value;
3540
3541 let raw: Value = serde_json::from_str(
3542 r#"{
3543 "type":"system","subtype":"feedback_draft_queued",
3544 "draft_id":"draft-1","draft_type":"bug_report",
3545 "title":"Tool output was truncated",
3546 "details_preview":"The last command omitted its final lines",
3547 "uuid":"u1","session_id":"s1","future_field":"preserved"
3548 }"#,
3549 )
3550 .unwrap();
3551 crate::io::assert_fully_wrapped(&raw);
3552
3553 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3554 let ClaudeOutput::System(sys) = output else {
3555 panic!("expected System");
3556 };
3557 assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3558 assert!(sys.is_feedback_draft_queued());
3559 assert!(!sys.is_vcs_state_changed());
3560
3561 let direct = sys
3562 .as_feedback_draft_queued()
3563 .expect("direct typed accessor");
3564 assert_eq!(direct.draft_id, "draft-1");
3565 assert_eq!(direct.draft_type, "bug_report");
3566 assert_eq!(direct.extra["future_field"], "preserved");
3567
3568 let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3569 panic!("expected FeedbackDraftQueued event");
3570 };
3571 assert_eq!(known.title, "Tool output was truncated");
3572 assert_eq!(
3573 sys.typed_value().expect("typed value")["future_field"],
3574 "preserved"
3575 );
3576 }
3577
3578 #[test]
3579 fn test_cloud_session_delta_fully_wrapped() {
3580 use super::{KnownSystemEvent, SystemSubtype};
3581 use serde_json::Value;
3582
3583 let raw: Value = serde_json::from_str(
3584 r#"{
3585 "type":"system","subtype":"cloud_session_delta",
3586 "seq":3,"changed":["serving","connection"],
3587 "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
3588 "serving":{"state":"on"},"connection":{"state":"live"}},
3589 "uuid":"u1","session_id":"s1","future_field":"preserved"
3590 }"#,
3591 )
3592 .unwrap();
3593 crate::io::assert_fully_wrapped(&raw);
3594
3595 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3596 let ClaudeOutput::System(sys) = output else {
3597 panic!("expected System");
3598 };
3599 assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
3600 assert!(sys.is_cloud_session_delta());
3601 assert!(!sys.is_feedback_draft_queued());
3602
3603 let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
3604 assert_eq!(direct.seq, 3);
3605 assert_eq!(direct.changed, vec!["serving", "connection"]);
3606 assert_eq!(direct.cloud_session["id"], "session_abc");
3607 assert_eq!(direct.extra["future_field"], "preserved");
3608
3609 let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
3610 panic!("expected CloudSessionDelta event");
3611 };
3612 assert_eq!(known.session_id, "s1");
3613 assert_eq!(
3614 sys.typed_value().expect("typed value")["future_field"],
3615 "preserved"
3616 );
3617 }
3618
3619 #[test]
3620 fn test_vcs_state_changed_fully_wrapped() {
3621 use super::{KnownSystemEvent, VcsMutationKind};
3622 use serde_json::Value;
3623
3624 for kind in ["commit", "push", "merge", "rebase"] {
3625 let raw: Value = serde_json::from_str(&format!(
3626 r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3627 kind
3628 ))
3629 .unwrap();
3630 crate::io::assert_fully_wrapped(&raw);
3631
3632 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3633 let ClaudeOutput::System(sys) = output else {
3634 panic!("expected System");
3635 };
3636 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3637 panic!("expected VcsStateChanged event");
3638 };
3639 assert_eq!(msg.kind.as_str(), kind);
3640 assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3641 }
3642
3643 let raw: Value = serde_json::from_str(
3645 r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3646 )
3647 .unwrap();
3648 crate::io::assert_fully_wrapped(&raw);
3649 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3650 let ClaudeOutput::System(sys) = output else {
3651 panic!("expected System");
3652 };
3653 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3654 panic!("expected VcsStateChanged event");
3655 };
3656 assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3657
3658 assert!(sys.is_vcs_state_changed());
3659 let direct = sys.as_vcs_state_changed().expect("direct accessor");
3660 assert_eq!(direct.cwd, "/repo");
3661 assert!(sys.as_code_change_published().is_none());
3662 }
3663
3664 #[test]
3665 fn test_assistant_aborted_and_resume_flags_roundtrip() {
3666 let json = r#"{
3667 "type":"assistant",
3668 "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3669 "session_id":"s1",
3670 "aborted":true,
3671 "resumed_from_incomplete_thinking":true
3672 }"#;
3673 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3674 let ClaudeOutput::Assistant(msg) = &output else {
3675 panic!("expected Assistant");
3676 };
3677 assert_eq!(msg.aborted, Some(true));
3678 assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3679 let reserialized = serde_json::to_string(&output).unwrap();
3680 assert!(reserialized.contains("\"aborted\":true"));
3681 assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3682
3683 let json = r#"{
3685 "type":"assistant",
3686 "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3687 "session_id":"s2"
3688 }"#;
3689 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3690 let reserialized = serde_json::to_string(&output).unwrap();
3691 assert!(!reserialized.contains("aborted"));
3692 assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3693 }
3694
3695 #[test]
3696 fn test_user_tool_result_meta_roundtrip() {
3697 let json = r#"{
3698 "type":"user",
3699 "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3700 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3701 "tool_result_meta":[
3702 {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3703 {"id":"toolu_2","non_execution_kind":"permission-rule"}
3704 ]
3705 }"#;
3706 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3707 let ClaudeOutput::User(user) = &output else {
3708 panic!("expected User");
3709 };
3710 let meta = user.tool_result_meta.as_ref().unwrap();
3711 assert_eq!(meta.len(), 2);
3712 assert_eq!(meta[0].non_execution_kind, "user-rejected");
3713 assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3714 assert_eq!(meta[1].user_feedback, None);
3715
3716 let reserialized = serde_json::to_string(&output).unwrap();
3717 assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3718 assert!(!reserialized.contains("\"user_feedback\":null"));
3719 }
3720
3721 #[test]
3725 fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3726 use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3727 let with_scope = serde_json::json!({
3728 "trigger": "refusal",
3729 "direction": "retry",
3730 "scope": "local",
3731 "original_model": "claude-fable-5",
3732 "fallback_model": "claude-opus-5",
3733 "request_id": null,
3734 "content": "Refused; retried on fallback model.",
3735 "uuid": "u1",
3736 "session_id": "s1"
3737 });
3738 let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3739 assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3740 assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3741
3742 let mut without = with_scope.clone();
3745 without.as_object_mut().unwrap().remove("scope");
3746 let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3747 assert_eq!(msg.scope, None);
3748 assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3749
3750 assert_eq!(
3752 RefusalFallbackScope::from("workspace").as_str(),
3753 "workspace"
3754 );
3755 }
3756}