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