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 Unknown(String),
46}
47
48impl SystemSubtype {
49 pub fn as_str(&self) -> &str {
50 match self {
51 Self::Init => "init",
52 Self::Status => "status",
53 Self::CompactBoundary => "compact_boundary",
54 Self::ThinkingTokens => "thinking_tokens",
55 Self::TaskStarted => "task_started",
56 Self::TaskProgress => "task_progress",
57 Self::TaskUpdated => "task_updated",
58 Self::TaskNotification => "task_notification",
59 Self::ApiRetry => "api_retry",
60 Self::ControlRequestProgress => "control_request_progress",
61 Self::ModelRefusalFallback => "model_refusal_fallback",
62 Self::ModelRefusalNoFallback => "model_refusal_no_fallback",
63 Self::LocalCommandOutput => "local_command_output",
64 Self::HookStarted => "hook_started",
65 Self::HookProgress => "hook_progress",
66 Self::HookResponse => "hook_response",
67 Self::PluginInstall => "plugin_install",
68 Self::BackgroundTasksChanged => "background_tasks_changed",
69 Self::SessionStateChanged => "session_state_changed",
70 Self::WorkerShuttingDown => "worker_shutting_down",
71 Self::CommandsChanged => "commands_changed",
72 Self::Notification => "notification",
73 Self::FilesPersisted => "files_persisted",
74 Self::MemoryRecall => "memory_recall",
75 Self::ElicitationComplete => "elicitation_complete",
76 Self::PermissionDenied => "permission_denied",
77 Self::MirrorError => "mirror_error",
78 Self::Informational => "informational",
79 Self::Unknown(s) => s.as_str(),
80 }
81 }
82}
83
84impl fmt::Display for SystemSubtype {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(self.as_str())
87 }
88}
89
90impl From<&str> for SystemSubtype {
91 fn from(s: &str) -> Self {
92 match s {
93 "init" => Self::Init,
94 "status" => Self::Status,
95 "compact_boundary" => Self::CompactBoundary,
96 "thinking_tokens" => Self::ThinkingTokens,
97 "task_started" => Self::TaskStarted,
98 "task_progress" => Self::TaskProgress,
99 "task_updated" => Self::TaskUpdated,
100 "task_notification" => Self::TaskNotification,
101 "api_retry" => Self::ApiRetry,
102 "control_request_progress" => Self::ControlRequestProgress,
103 "model_refusal_fallback" => Self::ModelRefusalFallback,
104 "model_refusal_no_fallback" => Self::ModelRefusalNoFallback,
105 "local_command_output" => Self::LocalCommandOutput,
106 "hook_started" => Self::HookStarted,
107 "hook_progress" => Self::HookProgress,
108 "hook_response" => Self::HookResponse,
109 "plugin_install" => Self::PluginInstall,
110 "background_tasks_changed" => Self::BackgroundTasksChanged,
111 "session_state_changed" => Self::SessionStateChanged,
112 "worker_shutting_down" => Self::WorkerShuttingDown,
113 "commands_changed" => Self::CommandsChanged,
114 "notification" => Self::Notification,
115 "files_persisted" => Self::FilesPersisted,
116 "memory_recall" => Self::MemoryRecall,
117 "elicitation_complete" => Self::ElicitationComplete,
118 "permission_denied" => Self::PermissionDenied,
119 "mirror_error" => Self::MirrorError,
120 "informational" => Self::Informational,
121 other => Self::Unknown(other.to_string()),
122 }
123 }
124}
125
126impl Serialize for SystemSubtype {
127 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
128 serializer.serialize_str(self.as_str())
129 }
130}
131
132impl<'de> Deserialize<'de> for SystemSubtype {
133 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
134 let s = String::deserialize(deserializer)?;
135 Ok(Self::from(s.as_str()))
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Hash)]
144pub enum MessageRole {
145 User,
146 Assistant,
147 Unknown(String),
149}
150
151impl MessageRole {
152 pub fn as_str(&self) -> &str {
153 match self {
154 Self::User => "user",
155 Self::Assistant => "assistant",
156 Self::Unknown(s) => s.as_str(),
157 }
158 }
159}
160
161impl fmt::Display for MessageRole {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.write_str(self.as_str())
164 }
165}
166
167impl From<&str> for MessageRole {
168 fn from(s: &str) -> Self {
169 match s {
170 "user" => Self::User,
171 "assistant" => Self::Assistant,
172 other => Self::Unknown(other.to_string()),
173 }
174 }
175}
176
177impl Serialize for MessageRole {
178 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
179 serializer.serialize_str(self.as_str())
180 }
181}
182
183impl<'de> Deserialize<'de> for MessageRole {
184 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
185 let s = String::deserialize(deserializer)?;
186 Ok(Self::from(s.as_str()))
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Hash)]
192pub enum CompactionTrigger {
193 Auto,
195 Manual,
197 Unknown(String),
199}
200
201impl CompactionTrigger {
202 pub fn as_str(&self) -> &str {
203 match self {
204 Self::Auto => "auto",
205 Self::Manual => "manual",
206 Self::Unknown(s) => s.as_str(),
207 }
208 }
209}
210
211impl fmt::Display for CompactionTrigger {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 f.write_str(self.as_str())
214 }
215}
216
217impl From<&str> for CompactionTrigger {
218 fn from(s: &str) -> Self {
219 match s {
220 "auto" => Self::Auto,
221 "manual" => Self::Manual,
222 other => Self::Unknown(other.to_string()),
223 }
224 }
225}
226
227impl Serialize for CompactionTrigger {
228 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
229 serializer.serialize_str(self.as_str())
230 }
231}
232
233impl<'de> Deserialize<'de> for CompactionTrigger {
234 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
235 let s = String::deserialize(deserializer)?;
236 Ok(Self::from(s.as_str()))
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Hash)]
242pub enum StopReason {
243 EndTurn,
245 MaxTokens,
247 ToolUse,
249 Unknown(String),
251}
252
253impl StopReason {
254 pub fn as_str(&self) -> &str {
255 match self {
256 Self::EndTurn => "end_turn",
257 Self::MaxTokens => "max_tokens",
258 Self::ToolUse => "tool_use",
259 Self::Unknown(s) => s.as_str(),
260 }
261 }
262}
263
264impl fmt::Display for StopReason {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 f.write_str(self.as_str())
267 }
268}
269
270impl From<&str> for StopReason {
271 fn from(s: &str) -> Self {
272 match s {
273 "end_turn" => Self::EndTurn,
274 "max_tokens" => Self::MaxTokens,
275 "tool_use" => Self::ToolUse,
276 other => Self::Unknown(other.to_string()),
277 }
278 }
279}
280
281impl Serialize for StopReason {
282 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
283 serializer.serialize_str(self.as_str())
284 }
285}
286
287impl<'de> Deserialize<'de> for StopReason {
288 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
289 let s = String::deserialize(deserializer)?;
290 Ok(Self::from(s.as_str()))
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, Hash)]
296pub enum ApiKeySource {
297 None,
299 User,
300 Project,
301 Org,
302 Temporary,
303 Oauth,
304 Unknown(String),
306}
307
308impl ApiKeySource {
309 pub fn as_str(&self) -> &str {
310 match self {
311 Self::None => "none",
312 Self::User => "user",
313 Self::Project => "project",
314 Self::Org => "org",
315 Self::Temporary => "temporary",
316 Self::Oauth => "oauth",
317 Self::Unknown(s) => s.as_str(),
318 }
319 }
320}
321
322impl fmt::Display for ApiKeySource {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 f.write_str(self.as_str())
325 }
326}
327
328impl From<&str> for ApiKeySource {
329 fn from(s: &str) -> Self {
330 match s {
331 "none" => Self::None,
332 "user" => Self::User,
333 "project" => Self::Project,
334 "org" => Self::Org,
335 "temporary" => Self::Temporary,
336 "oauth" => Self::Oauth,
337 other => Self::Unknown(other.to_string()),
338 }
339 }
340}
341
342impl Serialize for ApiKeySource {
343 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
344 serializer.serialize_str(self.as_str())
345 }
346}
347
348impl<'de> Deserialize<'de> for ApiKeySource {
349 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
350 let s = String::deserialize(deserializer)?;
351 Ok(Self::from(s.as_str()))
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Hash)]
357pub enum OutputStyle {
358 Default,
360 Unknown(String),
362}
363
364impl OutputStyle {
365 pub fn as_str(&self) -> &str {
366 match self {
367 Self::Default => "default",
368 Self::Unknown(s) => s.as_str(),
369 }
370 }
371}
372
373impl fmt::Display for OutputStyle {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 f.write_str(self.as_str())
376 }
377}
378
379impl From<&str> for OutputStyle {
380 fn from(s: &str) -> Self {
381 match s {
382 "default" => Self::Default,
383 other => Self::Unknown(other.to_string()),
384 }
385 }
386}
387
388impl Serialize for OutputStyle {
389 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
390 serializer.serialize_str(self.as_str())
391 }
392}
393
394impl<'de> Deserialize<'de> for OutputStyle {
395 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
396 let s = String::deserialize(deserializer)?;
397 Ok(Self::from(s.as_str()))
398 }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Hash)]
403pub enum InitPermissionMode {
404 Default,
406 AcceptEdits,
407 BypassPermissions,
408 Plan,
409 DontAsk,
410 Auto,
411 Unknown(String),
413}
414
415impl InitPermissionMode {
416 pub fn as_str(&self) -> &str {
417 match self {
418 Self::Default => "default",
419 Self::AcceptEdits => "acceptEdits",
420 Self::BypassPermissions => "bypassPermissions",
421 Self::Plan => "plan",
422 Self::DontAsk => "dontAsk",
423 Self::Auto => "auto",
424 Self::Unknown(s) => s.as_str(),
425 }
426 }
427}
428
429impl fmt::Display for InitPermissionMode {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 f.write_str(self.as_str())
432 }
433}
434
435impl From<&str> for InitPermissionMode {
436 fn from(s: &str) -> Self {
437 match s {
438 "default" => Self::Default,
439 "acceptEdits" => Self::AcceptEdits,
440 "bypassPermissions" => Self::BypassPermissions,
441 "plan" => Self::Plan,
442 "dontAsk" => Self::DontAsk,
443 "auto" => Self::Auto,
444 other => Self::Unknown(other.to_string()),
445 }
446 }
447}
448
449impl Serialize for InitPermissionMode {
450 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
451 serializer.serialize_str(self.as_str())
452 }
453}
454
455impl<'de> Deserialize<'de> for InitPermissionMode {
456 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
457 let s = String::deserialize(deserializer)?;
458 Ok(Self::from(s.as_str()))
459 }
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Hash)]
464pub enum StatusMessageStatus {
465 Compacting,
467 Requesting,
469 Unknown(String),
471}
472
473impl StatusMessageStatus {
474 pub fn as_str(&self) -> &str {
475 match self {
476 Self::Compacting => "compacting",
477 Self::Requesting => "requesting",
478 Self::Unknown(s) => s.as_str(),
479 }
480 }
481}
482
483impl fmt::Display for StatusMessageStatus {
484 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485 f.write_str(self.as_str())
486 }
487}
488
489impl From<&str> for StatusMessageStatus {
490 fn from(s: &str) -> Self {
491 match s {
492 "compacting" => Self::Compacting,
493 "requesting" => Self::Requesting,
494 other => Self::Unknown(other.to_string()),
495 }
496 }
497}
498
499impl Serialize for StatusMessageStatus {
500 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
501 serializer.serialize_str(self.as_str())
502 }
503}
504
505impl<'de> Deserialize<'de> for StatusMessageStatus {
506 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
507 let s = String::deserialize(deserializer)?;
508 Ok(Self::from(s.as_str()))
509 }
510}
511
512pub(crate) fn serialize_optional_uuid<S>(
514 uuid: &Option<Uuid>,
515 serializer: S,
516) -> Result<S::Ok, S::Error>
517where
518 S: Serializer,
519{
520 match uuid {
521 Some(id) => serializer.serialize_str(&id.to_string()),
522 None => serializer.serialize_none(),
523 }
524}
525
526pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
528where
529 D: Deserializer<'de>,
530{
531 let opt_str: Option<String> = Option::deserialize(deserializer)?;
532 match opt_str {
533 Some(s) => Uuid::parse_str(&s)
534 .map(Some)
535 .map_err(serde::de::Error::custom),
536 None => Ok(None),
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
543pub struct MessageOrigin {
544 pub kind: String,
545 #[serde(flatten)]
546 pub extra: serde_json::Map<String, Value>,
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
551pub struct SummarizeMetadata {
552 pub messages_summarized: u64,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub user_context: Option<String>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub direction: Option<String>,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
561pub struct McpMeta {
562 #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
563 pub meta: Option<Value>,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
565 pub structured_content: Option<Value>,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct UserMessage {
571 pub message: MessageContent,
572 #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
573 #[serde(
574 serialize_with = "serialize_optional_uuid",
575 deserialize_with = "deserialize_optional_uuid"
576 )]
577 pub session_id: Option<Uuid>,
578 #[serde(skip_serializing_if = "Option::is_none")]
580 pub parent_tool_use_id: Option<String>,
581 #[serde(skip_serializing_if = "Option::is_none")]
583 pub uuid: Option<String>,
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub timestamp: Option<String>,
587 #[serde(skip_serializing_if = "Option::is_none")]
594 pub tool_use_result: Option<serde_json::Value>,
595 #[serde(skip_serializing_if = "Option::is_none")]
598 pub subagent_type: Option<String>,
599 #[serde(skip_serializing_if = "Option::is_none")]
601 pub task_description: Option<String>,
602 #[serde(skip_serializing_if = "Option::is_none")]
603 pub origin: Option<MessageOrigin>,
604 #[serde(skip_serializing_if = "Option::is_none")]
605 pub priority: Option<String>,
606 #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
607 pub is_synthetic: Option<bool>,
608 #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
609 pub should_query: Option<bool>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub is_meta: Option<bool>,
612 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub is_visible_in_transcript_only: Option<bool>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub is_virtual: Option<bool>,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub is_compact_summary: Option<bool>,
618 #[serde(skip_serializing_if = "Option::is_none")]
619 pub summarize_metadata: Option<SummarizeMetadata>,
620 #[serde(skip_serializing_if = "Option::is_none")]
621 pub mcp_meta: Option<McpMeta>,
622 #[serde(skip_serializing_if = "Option::is_none")]
623 pub source_tool_use_id: Option<String>,
624 #[serde(skip_serializing_if = "Option::is_none")]
625 pub source_tool_assistant_uuid: Option<String>,
626 #[serde(skip_serializing_if = "Option::is_none")]
627 pub image_paste_ids: Option<Vec<u64>>,
628 #[serde(skip_serializing_if = "Option::is_none")]
629 pub client_platform: Option<String>,
630 #[serde(skip_serializing_if = "Option::is_none")]
631 pub inbound_origin: Option<String>,
632 #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
633 pub is_replay: Option<bool>,
634 #[serde(skip_serializing_if = "Option::is_none")]
635 pub file_attachments: Option<Vec<Value>>,
636}
637
638impl UserMessage {
639 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
647 &self,
648 ) -> Option<Result<T, serde_json::Error>> {
649 self.tool_use_result
650 .as_ref()
651 .map(|v| serde_json::from_value(v.clone()))
652 }
653
654 pub fn subagent_result(&self) -> Option<SubagentResult> {
665 self.tool_use_result
666 .as_ref()
667 .and_then(|v| serde_json::from_value(v.clone()).ok())
668 }
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct SubagentResult {
683 #[serde(skip_serializing_if = "Option::is_none")]
685 pub status: Option<String>,
686 #[serde(skip_serializing_if = "Option::is_none")]
688 pub prompt: Option<String>,
689 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
691 pub agent_id: Option<String>,
692 #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
694 pub agent_type: Option<String>,
695 #[serde(
697 default,
698 deserialize_with = "deserialize_content_blocks",
699 skip_serializing_if = "Vec::is_empty"
700 )]
701 pub content: Vec<ContentBlock>,
702 #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
704 pub resolved_model: Option<String>,
705 #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
707 pub total_duration_ms: Option<u64>,
708 #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
710 pub total_tokens: Option<u64>,
711 #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
713 pub total_tool_use_count: Option<u64>,
714 #[serde(skip_serializing_if = "Option::is_none")]
716 pub usage: Option<super::result::UsageInfo>,
717 #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
719 pub tool_stats: Option<SubagentToolStats>,
720}
721
722#[derive(Debug, Clone, Default, Serialize, Deserialize)]
727#[serde(rename_all = "camelCase")]
728pub struct SubagentToolStats {
729 #[serde(default)]
730 pub read_count: u64,
731 #[serde(default)]
732 pub search_count: u64,
733 #[serde(default)]
734 pub bash_count: u64,
735 #[serde(default)]
736 pub edit_file_count: u64,
737 #[serde(default)]
738 pub lines_added: u64,
739 #[serde(default)]
740 pub lines_removed: u64,
741 #[serde(default)]
742 pub other_tool_count: u64,
743 #[serde(flatten)]
744 pub extra: serde_json::Map<String, Value>,
745}
746
747#[derive(Debug, Clone, Default, PartialEq, Eq)]
774pub struct SubagentUsageRollup {
775 pub subagent_tokens: u64,
778 pub agent_count: u32,
780 pub tool_uses: u64,
782 pub duration_ms: u64,
784 seen_agent_ids: std::collections::BTreeSet<String>,
785}
786
787impl SubagentUsageRollup {
788 pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
794 match output {
795 ClaudeOutput::User(user) => self.observe_user(user),
796 _ => false,
797 }
798 }
799
800 pub fn observe_user(&mut self, user: &UserMessage) -> bool {
807 let Some(result) = user.subagent_result() else {
808 return false;
809 };
810 if result.total_tokens.is_none() && result.agent_id.is_none() {
811 return false;
812 }
813 if let Some(agent_id) = &result.agent_id {
814 if !self.seen_agent_ids.insert(agent_id.clone()) {
815 return false;
816 }
817 }
818 self.agent_count += 1;
819 self.subagent_tokens += result.total_tokens.unwrap_or(0);
820 self.tool_uses += result.total_tool_use_count.unwrap_or(0);
821 self.duration_ms += result.total_duration_ms.unwrap_or(0);
822 true
823 }
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize)]
828pub struct MessageContent {
829 pub role: MessageRole,
830 #[serde(deserialize_with = "deserialize_content_blocks")]
831 pub content: Vec<ContentBlock>,
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
836pub struct SystemMessage {
837 pub subtype: SystemSubtype,
838 #[serde(flatten)]
839 pub data: Value, }
841
842impl SystemMessage {
843 pub fn is_init(&self) -> bool {
845 self.subtype == SystemSubtype::Init
846 }
847
848 pub fn is_status(&self) -> bool {
850 self.subtype == SystemSubtype::Status
851 }
852
853 pub fn is_compact_boundary(&self) -> bool {
855 self.subtype == SystemSubtype::CompactBoundary
856 }
857
858 pub fn as_init(&self) -> Option<InitMessage> {
860 if self.subtype != SystemSubtype::Init {
861 return None;
862 }
863 serde_json::from_value(self.data.clone()).ok()
864 }
865
866 pub fn as_status(&self) -> Option<StatusMessage> {
868 if self.subtype != SystemSubtype::Status {
869 return None;
870 }
871 serde_json::from_value(self.data.clone()).ok()
872 }
873
874 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
876 if self.subtype != SystemSubtype::CompactBoundary {
877 return None;
878 }
879 serde_json::from_value(self.data.clone()).ok()
880 }
881
882 pub fn is_task_started(&self) -> bool {
884 self.subtype == SystemSubtype::TaskStarted
885 }
886
887 pub fn is_task_progress(&self) -> bool {
889 self.subtype == SystemSubtype::TaskProgress
890 }
891
892 pub fn is_task_notification(&self) -> bool {
894 self.subtype == SystemSubtype::TaskNotification
895 }
896
897 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
899 if self.subtype != SystemSubtype::TaskStarted {
900 return None;
901 }
902 serde_json::from_value(self.data.clone()).ok()
903 }
904
905 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
907 if self.subtype != SystemSubtype::TaskProgress {
908 return None;
909 }
910 serde_json::from_value(self.data.clone()).ok()
911 }
912
913 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
915 if self.subtype != SystemSubtype::TaskNotification {
916 return None;
917 }
918 serde_json::from_value(self.data.clone()).ok()
919 }
920
921 pub fn is_task_updated(&self) -> bool {
923 self.subtype == SystemSubtype::TaskUpdated
924 }
925
926 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
928 if self.subtype != SystemSubtype::TaskUpdated {
929 return None;
930 }
931 serde_json::from_value(self.data.clone()).ok()
932 }
933
934 pub fn is_thinking_tokens(&self) -> bool {
936 self.subtype == SystemSubtype::ThinkingTokens
937 }
938
939 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
941 if self.subtype != SystemSubtype::ThinkingTokens {
942 return None;
943 }
944 serde_json::from_value(self.data.clone()).ok()
945 }
946
947 pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
949 macro_rules! parse {
950 ($variant:ident, $ty:ty) => {
951 serde_json::from_value::<$ty>(self.data.clone())
952 .ok()
953 .map(KnownSystemEvent::$variant)
954 };
955 }
956
957 match self.subtype {
958 SystemSubtype::Init => parse!(Init, InitMessage),
959 SystemSubtype::Status => parse!(Status, StatusMessage),
960 SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
961 SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
962 SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
963 SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
964 SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
965 SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
966 SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
967 SystemSubtype::ControlRequestProgress => {
968 parse!(ControlRequestProgress, ControlRequestProgressMessage)
969 }
970 SystemSubtype::ModelRefusalFallback => {
971 parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
972 }
973 SystemSubtype::ModelRefusalNoFallback => {
974 parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
975 }
976 SystemSubtype::LocalCommandOutput => {
977 parse!(LocalCommandOutput, LocalCommandOutputMessage)
978 }
979 SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
980 SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
981 SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
982 SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
983 SystemSubtype::BackgroundTasksChanged => {
984 parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
985 }
986 SystemSubtype::SessionStateChanged => {
987 parse!(SessionStateChanged, SessionStateChangedMessage)
988 }
989 SystemSubtype::WorkerShuttingDown => {
990 parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
991 }
992 SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
993 SystemSubtype::Notification => parse!(Notification, NotificationMessage),
994 SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
995 SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
996 SystemSubtype::ElicitationComplete => {
997 parse!(ElicitationComplete, ElicitationCompleteMessage)
998 }
999 SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1000 SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1001 SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1002 SystemSubtype::Unknown(_) => None,
1003 }
1004 }
1005
1006 pub fn typed_value(&self) -> Option<Value> {
1015 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1016 parsed.and_then(|v| serde_json::to_value(v).ok())
1017 }
1018 match self.subtype {
1019 SystemSubtype::Init => reserialize(self.as_init()),
1020 SystemSubtype::Status => reserialize(self.as_status()),
1021 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1022 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1023 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1024 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1025 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1026 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1027 SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1028 SystemSubtype::ControlRequestProgress => {
1029 reserialize(parse_system::<ControlRequestProgressMessage>(self))
1030 }
1031 SystemSubtype::ModelRefusalFallback => {
1032 reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1033 }
1034 SystemSubtype::ModelRefusalNoFallback => {
1035 reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1036 }
1037 SystemSubtype::LocalCommandOutput => {
1038 reserialize(parse_system::<LocalCommandOutputMessage>(self))
1039 }
1040 SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1041 SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1042 SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1043 SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1044 SystemSubtype::BackgroundTasksChanged => {
1045 reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1046 }
1047 SystemSubtype::SessionStateChanged => {
1048 reserialize(parse_system::<SessionStateChangedMessage>(self))
1049 }
1050 SystemSubtype::WorkerShuttingDown => {
1051 reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1052 }
1053 SystemSubtype::CommandsChanged => {
1054 reserialize(parse_system::<CommandsChangedMessage>(self))
1055 }
1056 SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1057 SystemSubtype::FilesPersisted => {
1058 reserialize(parse_system::<FilesPersistedMessage>(self))
1059 }
1060 SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1061 SystemSubtype::ElicitationComplete => {
1062 reserialize(parse_system::<ElicitationCompleteMessage>(self))
1063 }
1064 SystemSubtype::PermissionDenied => {
1065 reserialize(parse_system::<PermissionDeniedMessage>(self))
1066 }
1067 SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1068 SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1069 SystemSubtype::Unknown(_) => None,
1070 }
1071 }
1072}
1073
1074fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1075 serde_json::from_value(message.data.clone()).ok()
1076}
1077
1078#[derive(Debug, Clone, Serialize, Deserialize)]
1080pub enum KnownSystemEvent {
1081 Init(InitMessage),
1082 Status(StatusMessage),
1083 CompactBoundary(CompactBoundaryMessage),
1084 ThinkingTokens(ThinkingTokensMessage),
1085 TaskStarted(TaskStartedMessage),
1086 TaskProgress(TaskProgressMessage),
1087 TaskUpdated(TaskUpdatedMessage),
1088 TaskNotification(TaskNotificationMessage),
1089 ApiRetry(ApiRetryMessage),
1090 ControlRequestProgress(ControlRequestProgressMessage),
1091 ModelRefusalFallback(ModelRefusalFallbackMessage),
1092 ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1093 LocalCommandOutput(LocalCommandOutputMessage),
1094 HookStarted(HookStartedMessage),
1095 HookProgress(HookProgressMessage),
1096 HookResponse(HookResponseMessage),
1097 PluginInstall(PluginInstallMessage),
1098 BackgroundTasksChanged(BackgroundTasksChangedMessage),
1099 SessionStateChanged(SessionStateChangedMessage),
1100 WorkerShuttingDown(WorkerShuttingDownMessage),
1101 CommandsChanged(CommandsChangedMessage),
1102 Notification(NotificationMessage),
1103 FilesPersisted(FilesPersistedMessage),
1104 MemoryRecall(MemoryRecallMessage),
1105 ElicitationComplete(ElicitationCompleteMessage),
1106 PermissionDenied(PermissionDeniedMessage),
1107 MirrorError(MirrorErrorMessage),
1108 Informational(InformationalMessage),
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize)]
1112pub struct ApiRetryMessage {
1113 pub attempt: u64,
1114 pub max_retries: u64,
1115 pub retry_delay_ms: u64,
1116 pub error_status: Option<u16>,
1117 pub error: String,
1118 #[serde(default, skip_serializing_if = "Option::is_none")]
1119 pub uuid: Option<String>,
1120 #[serde(default, skip_serializing_if = "Option::is_none")]
1121 pub session_id: Option<String>,
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize)]
1125pub struct ControlRequestProgressMessage {
1126 pub request_id: String,
1127 pub status: String,
1128 #[serde(default, skip_serializing_if = "Option::is_none")]
1129 pub attempt: Option<u64>,
1130 #[serde(default, skip_serializing_if = "Option::is_none")]
1131 pub max_retries: Option<u64>,
1132 #[serde(default, skip_serializing_if = "Option::is_none")]
1133 pub retry_delay_ms: Option<u64>,
1134 #[serde(default, skip_serializing_if = "Option::is_none")]
1135 pub error_status: Option<u16>,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub error: Option<String>,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1139 pub uuid: Option<String>,
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1141 pub session_id: Option<String>,
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1145pub struct ModelRefusalFallbackMessage {
1146 pub trigger: String,
1147 pub direction: String,
1148 pub original_model: String,
1149 pub fallback_model: String,
1150 pub request_id: Option<String>,
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1152 pub api_refusal_category: Option<String>,
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub api_refusal_explanation: Option<String>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub retracted_message_uuids: Option<Vec<String>>,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 pub refused_user_message_uuid: Option<String>,
1159 pub content: Value,
1160 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub uuid: Option<String>,
1162 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub session_id: Option<String>,
1164}
1165
1166#[derive(Debug, Clone, Serialize, Deserialize)]
1167pub struct ModelRefusalNoFallbackMessage {
1168 pub original_model: String,
1169 pub request_id: Option<String>,
1170 #[serde(default, skip_serializing_if = "Option::is_none")]
1171 pub api_refusal_category: Option<String>,
1172 #[serde(default, skip_serializing_if = "Option::is_none")]
1173 pub api_refusal_explanation: Option<String>,
1174 pub content: Value,
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub uuid: Option<String>,
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1178 pub session_id: Option<String>,
1179}
1180
1181#[derive(Debug, Clone, Serialize, Deserialize)]
1182pub struct LocalCommandOutputMessage {
1183 pub content: String,
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1185 pub uuid: Option<String>,
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1187 pub session_id: Option<String>,
1188}
1189
1190#[derive(Debug, Clone, Serialize, Deserialize)]
1191pub struct HookStartedMessage {
1192 pub hook_id: String,
1193 pub hook_name: String,
1194 pub hook_event: String,
1195 #[serde(default, skip_serializing_if = "Option::is_none")]
1196 pub uuid: Option<String>,
1197 #[serde(default, skip_serializing_if = "Option::is_none")]
1198 pub session_id: Option<String>,
1199}
1200
1201#[derive(Debug, Clone, Serialize, Deserialize)]
1202pub struct HookProgressMessage {
1203 pub hook_id: String,
1204 pub hook_name: String,
1205 pub hook_event: String,
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1207 pub stdout: Option<String>,
1208 #[serde(default, skip_serializing_if = "Option::is_none")]
1209 pub stderr: Option<String>,
1210 #[serde(default, skip_serializing_if = "Option::is_none")]
1211 pub output: Option<String>,
1212 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub uuid: Option<String>,
1214 #[serde(default, skip_serializing_if = "Option::is_none")]
1215 pub session_id: Option<String>,
1216}
1217
1218#[derive(Debug, Clone, Serialize, Deserialize)]
1219pub struct HookResponseMessage {
1220 pub hook_id: String,
1221 pub hook_name: String,
1222 pub hook_event: String,
1223 #[serde(default, skip_serializing_if = "Option::is_none")]
1224 pub stdout: Option<String>,
1225 #[serde(default, skip_serializing_if = "Option::is_none")]
1226 pub stderr: Option<String>,
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub output: Option<String>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub exit_code: Option<i32>,
1231 pub outcome: String,
1232 #[serde(default, skip_serializing_if = "Option::is_none")]
1233 pub uuid: Option<String>,
1234 #[serde(default, skip_serializing_if = "Option::is_none")]
1235 pub session_id: Option<String>,
1236}
1237
1238#[derive(Debug, Clone, Serialize, Deserialize)]
1239pub struct PluginInstallMessage {
1240 pub status: String,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1242 pub name: Option<String>,
1243 #[serde(default, skip_serializing_if = "Option::is_none")]
1244 pub error: Option<String>,
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub uuid: Option<String>,
1247 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub session_id: Option<String>,
1249}
1250
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1252pub struct BackgroundTasksChangedMessage {
1253 pub tasks: Vec<BackgroundTaskInfo>,
1254 #[serde(default, skip_serializing_if = "Option::is_none")]
1255 pub uuid: Option<String>,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub session_id: Option<String>,
1258}
1259
1260#[derive(Debug, Clone, Serialize, Deserialize)]
1261pub struct BackgroundTaskInfo {
1262 pub task_id: String,
1263 pub task_type: String,
1264 pub description: String,
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1268pub struct SessionStateChangedMessage {
1269 pub state: String,
1270 #[serde(default, skip_serializing_if = "Option::is_none")]
1271 pub uuid: Option<String>,
1272 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub session_id: Option<String>,
1274}
1275
1276#[derive(Debug, Clone, Serialize, Deserialize)]
1277pub struct WorkerShuttingDownMessage {
1278 pub reason: String,
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1280 pub uuid: Option<String>,
1281 #[serde(default, skip_serializing_if = "Option::is_none")]
1282 pub session_id: Option<String>,
1283}
1284
1285#[derive(Debug, Clone, Serialize, Deserialize)]
1286pub struct CommandsChangedMessage {
1287 pub commands: Vec<CommandInfo>,
1288 #[serde(default, skip_serializing_if = "Option::is_none")]
1289 pub uuid: Option<String>,
1290 #[serde(default, skip_serializing_if = "Option::is_none")]
1291 pub session_id: Option<String>,
1292}
1293
1294#[derive(Debug, Clone, Serialize, Deserialize)]
1295pub struct CommandInfo {
1296 pub name: String,
1297 pub description: String,
1298 #[serde(rename = "argumentHint")]
1299 pub argument_hint: String,
1300 #[serde(default, skip_serializing_if = "Option::is_none")]
1301 pub aliases: Option<Vec<String>>,
1302}
1303
1304#[derive(Debug, Clone, Serialize, Deserialize)]
1305pub struct NotificationMessage {
1306 pub key: String,
1307 pub text: String,
1308 pub priority: String,
1309 #[serde(default, skip_serializing_if = "Option::is_none")]
1310 pub color: Option<String>,
1311 #[serde(default, skip_serializing_if = "Option::is_none")]
1312 pub timeout_ms: Option<u64>,
1313 #[serde(default, skip_serializing_if = "Option::is_none")]
1314 pub uuid: Option<String>,
1315 #[serde(default, skip_serializing_if = "Option::is_none")]
1316 pub session_id: Option<String>,
1317}
1318
1319#[derive(Debug, Clone, Serialize, Deserialize)]
1320pub struct FilesPersistedMessage {
1321 pub files: Vec<PersistedFile>,
1322 pub failed: Vec<FailedPersistedFile>,
1323 pub processed_at: String,
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub uuid: Option<String>,
1326 #[serde(default, skip_serializing_if = "Option::is_none")]
1327 pub session_id: Option<String>,
1328}
1329
1330#[derive(Debug, Clone, Serialize, Deserialize)]
1331pub struct PersistedFile {
1332 pub filename: String,
1333 pub file_id: String,
1334}
1335
1336#[derive(Debug, Clone, Serialize, Deserialize)]
1337pub struct FailedPersistedFile {
1338 pub filename: String,
1339 pub error: String,
1340}
1341
1342#[derive(Debug, Clone, Serialize, Deserialize)]
1343pub struct MemoryRecallMessage {
1344 pub mode: String,
1345 pub memories: Vec<MemoryRecallItem>,
1346 #[serde(default, skip_serializing_if = "Option::is_none")]
1347 pub uuid: Option<String>,
1348 #[serde(default, skip_serializing_if = "Option::is_none")]
1349 pub session_id: Option<String>,
1350}
1351
1352#[derive(Debug, Clone, Serialize, Deserialize)]
1353pub struct MemoryRecallItem {
1354 pub path: String,
1355 pub scope: String,
1356 #[serde(default, skip_serializing_if = "Option::is_none")]
1357 pub content: Option<String>,
1358}
1359
1360#[derive(Debug, Clone, Serialize, Deserialize)]
1361pub struct ElicitationCompleteMessage {
1362 pub mcp_server_name: String,
1363 pub elicitation_id: String,
1364 #[serde(default, skip_serializing_if = "Option::is_none")]
1365 pub uuid: Option<String>,
1366 #[serde(default, skip_serializing_if = "Option::is_none")]
1367 pub session_id: Option<String>,
1368}
1369
1370#[derive(Debug, Clone, Serialize, Deserialize)]
1371pub struct PermissionDeniedMessage {
1372 pub tool_name: String,
1373 pub tool_use_id: String,
1374 #[serde(default, skip_serializing_if = "Option::is_none")]
1375 pub agent_id: Option<String>,
1376 #[serde(default, skip_serializing_if = "Option::is_none")]
1377 pub decision_reason_type: Option<String>,
1378 #[serde(default, skip_serializing_if = "Option::is_none")]
1379 pub decision_reason: Option<String>,
1380 pub message: String,
1381 #[serde(default, skip_serializing_if = "Option::is_none")]
1382 pub uuid: Option<String>,
1383 #[serde(default, skip_serializing_if = "Option::is_none")]
1384 pub session_id: Option<String>,
1385}
1386
1387#[derive(Debug, Clone, Serialize, Deserialize)]
1388pub struct MirrorErrorMessage {
1389 pub error: String,
1390 pub key: MirrorErrorKey,
1391 #[serde(default, skip_serializing_if = "Option::is_none")]
1392 pub uuid: Option<String>,
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1394 pub session_id: Option<String>,
1395}
1396
1397#[derive(Debug, Clone, Serialize, Deserialize)]
1398pub struct MirrorErrorKey {
1399 #[serde(rename = "projectKey")]
1400 pub project_key: String,
1401 #[serde(rename = "sessionId")]
1402 pub session_id: String,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1404 pub subpath: Option<String>,
1405}
1406
1407#[derive(Debug, Clone, Serialize, Deserialize)]
1408pub struct InformationalMessage {
1409 pub content: String,
1410 pub level: String,
1411 #[serde(default, skip_serializing_if = "Option::is_none")]
1412 pub tool_use_id: Option<String>,
1413 #[serde(default, skip_serializing_if = "Option::is_none")]
1414 pub prevent_continuation: Option<bool>,
1415 #[serde(default, skip_serializing_if = "Option::is_none")]
1416 pub uuid: Option<String>,
1417 #[serde(default, skip_serializing_if = "Option::is_none")]
1418 pub session_id: Option<String>,
1419}
1420
1421#[derive(Debug, Clone, Serialize, Deserialize)]
1423pub struct PluginInfo {
1424 pub name: String,
1426 pub path: String,
1428 #[serde(skip_serializing_if = "Option::is_none")]
1430 pub source: Option<String>,
1431}
1432
1433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1435pub struct PluginDiagnostic {
1436 pub plugin: String,
1437 #[serde(rename = "type")]
1438 pub diagnostic_type: String,
1439 pub message: String,
1440}
1441
1442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1444pub struct MemoryPaths {
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1446 pub auto: Option<String>,
1447 #[serde(default, skip_serializing_if = "Option::is_none")]
1448 pub team: Option<String>,
1449 #[serde(flatten)]
1450 pub extra: serde_json::Map<String, Value>,
1451}
1452
1453#[derive(Debug, Clone, Serialize, Deserialize)]
1455pub struct InitMessage {
1456 pub session_id: String,
1458 #[serde(skip_serializing_if = "Option::is_none")]
1460 pub cwd: Option<String>,
1461 #[serde(skip_serializing_if = "Option::is_none")]
1463 pub model: Option<String>,
1464 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1466 pub tools: Vec<String>,
1467 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1469 pub mcp_servers: Vec<Value>,
1470 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1472 pub slash_commands: Vec<String>,
1473 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1475 pub agents: Vec<String>,
1476 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1478 pub plugins: Vec<PluginInfo>,
1479 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1481 pub skills: Vec<Value>,
1482 #[serde(skip_serializing_if = "Option::is_none")]
1484 pub claude_code_version: Option<String>,
1485 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1487 pub api_key_source: Option<ApiKeySource>,
1488 #[serde(skip_serializing_if = "Option::is_none")]
1490 pub output_style: Option<OutputStyle>,
1491 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1493 pub permission_mode: Option<InitPermissionMode>,
1494
1495 #[serde(skip_serializing_if = "Option::is_none")]
1497 pub uuid: Option<String>,
1498
1499 #[serde(skip_serializing_if = "Option::is_none")]
1501 pub memory_paths: Option<MemoryPaths>,
1502
1503 #[serde(skip_serializing_if = "Option::is_none")]
1505 pub fast_mode_state: Option<String>,
1506
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1509 pub analytics_disabled: Option<bool>,
1510
1511 #[serde(default, skip_serializing_if = "Option::is_none")]
1513 pub product_feedback_disabled: Option<bool>,
1514
1515 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1517 pub betas: Vec<String>,
1518
1519 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1521 pub capabilities: Vec<String>,
1522
1523 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1525 pub plugin_errors: Vec<PluginDiagnostic>,
1526
1527 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1529 pub plugin_warnings: Vec<PluginDiagnostic>,
1530}
1531
1532#[derive(Debug, Clone, Serialize, Deserialize)]
1534pub struct StatusMessage {
1535 pub session_id: String,
1537 pub status: Option<StatusMessageStatus>,
1539 #[serde(skip_serializing_if = "Option::is_none")]
1541 pub uuid: Option<String>,
1542 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1544 pub permission_mode: Option<InitPermissionMode>,
1545 #[serde(skip_serializing_if = "Option::is_none")]
1546 pub compact_result: Option<String>,
1547 #[serde(skip_serializing_if = "Option::is_none")]
1548 pub compact_error: Option<String>,
1549}
1550
1551#[derive(Debug, Clone, Serialize, Deserialize)]
1553pub struct CompactBoundaryMessage {
1554 pub session_id: String,
1556 pub compact_metadata: CompactMetadata,
1558 #[serde(
1562 default,
1563 skip_serializing_if = "Option::is_none",
1564 alias = "content",
1565 alias = "text"
1566 )]
1567 pub summary: Option<String>,
1568 #[serde(
1572 default,
1573 skip_serializing_if = "Option::is_none",
1574 alias = "message_count"
1575 )]
1576 pub leaf_message_count: Option<u32>,
1577 #[serde(default, skip_serializing_if = "Option::is_none")]
1579 pub duration_ms: Option<u64>,
1580 #[serde(skip_serializing_if = "Option::is_none")]
1582 pub uuid: Option<String>,
1583 #[serde(skip_serializing_if = "Option::is_none")]
1585 pub logical_parent_uuid: Option<Option<String>>,
1586}
1587
1588#[derive(Debug, Clone, Serialize, Deserialize)]
1590pub struct CompactMetadata {
1591 pub pre_tokens: u64,
1593 pub trigger: CompactionTrigger,
1595 #[serde(default, skip_serializing_if = "Option::is_none")]
1596 pub post_tokens: Option<u64>,
1597 #[serde(default, skip_serializing_if = "Option::is_none")]
1598 pub cumulative_dropped_tokens: Option<u64>,
1599 #[serde(default, skip_serializing_if = "Option::is_none")]
1600 pub duration_ms: Option<u64>,
1601 #[serde(default, skip_serializing_if = "Option::is_none")]
1602 pub user_context: Option<String>,
1603 #[serde(default, skip_serializing_if = "Option::is_none")]
1604 pub messages_summarized: Option<u64>,
1605 #[serde(default, skip_serializing_if = "Option::is_none")]
1606 pub precomputed: Option<bool>,
1607 #[serde(default, skip_serializing_if = "Option::is_none")]
1608 pub pre_compact_discovered_tools: Option<Vec<String>>,
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1610 pub preserved_segment: Option<PreservedSegment>,
1611 #[serde(default, skip_serializing_if = "Option::is_none")]
1612 pub preserved_messages: Option<PreservedMessages>,
1613}
1614
1615#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1616pub struct PreservedSegment {
1617 pub head_uuid: String,
1618 pub anchor_uuid: String,
1619 pub tail_uuid: String,
1620}
1621
1622#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1623pub struct PreservedMessages {
1624 pub anchor_uuid: String,
1625 pub uuids: Vec<String>,
1626 #[serde(default, skip_serializing_if = "Option::is_none")]
1627 pub all_uuids: Option<Vec<String>>,
1628}
1629
1630#[derive(Debug, Clone, Serialize, Deserialize)]
1636pub struct TaskUsage {
1637 pub duration_ms: u64,
1639 pub tool_uses: u64,
1641 pub total_tokens: u64,
1643}
1644
1645#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1647pub enum TaskType {
1648 LocalAgent,
1650 LocalBash,
1652 LocalWorkflow,
1654 Unknown(String),
1656}
1657
1658impl TaskType {
1659 pub fn as_str(&self) -> &str {
1660 match self {
1661 Self::LocalAgent => "local_agent",
1662 Self::LocalBash => "local_bash",
1663 Self::LocalWorkflow => "local_workflow",
1664 Self::Unknown(s) => s.as_str(),
1665 }
1666 }
1667}
1668
1669impl fmt::Display for TaskType {
1670 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1671 f.write_str(self.as_str())
1672 }
1673}
1674
1675impl From<&str> for TaskType {
1676 fn from(s: &str) -> Self {
1677 match s {
1678 "local_agent" => Self::LocalAgent,
1679 "local_bash" => Self::LocalBash,
1680 "local_workflow" => Self::LocalWorkflow,
1681 other => Self::Unknown(other.to_string()),
1682 }
1683 }
1684}
1685
1686impl Serialize for TaskType {
1687 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1688 serializer.serialize_str(self.as_str())
1689 }
1690}
1691
1692impl<'de> Deserialize<'de> for TaskType {
1693 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1694 let s = String::deserialize(deserializer)?;
1695 Ok(Self::from(s.as_str()))
1696 }
1697}
1698
1699#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1701pub enum TaskStatus {
1702 Pending,
1703 Running,
1704 Completed,
1705 Failed,
1706 Killed,
1707 Paused,
1708 Stopped,
1709 Unknown(String),
1710}
1711
1712impl TaskStatus {
1713 pub fn as_str(&self) -> &str {
1714 match self {
1715 Self::Pending => "pending",
1716 Self::Running => "running",
1717 Self::Completed => "completed",
1718 Self::Failed => "failed",
1719 Self::Killed => "killed",
1720 Self::Paused => "paused",
1721 Self::Stopped => "stopped",
1722 Self::Unknown(s) => s.as_str(),
1723 }
1724 }
1725}
1726
1727impl fmt::Display for TaskStatus {
1728 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1729 f.write_str(self.as_str())
1730 }
1731}
1732
1733impl From<&str> for TaskStatus {
1734 fn from(s: &str) -> Self {
1735 match s {
1736 "pending" => Self::Pending,
1737 "running" => Self::Running,
1738 "completed" => Self::Completed,
1739 "failed" => Self::Failed,
1740 "killed" => Self::Killed,
1741 "paused" => Self::Paused,
1742 "stopped" => Self::Stopped,
1743 other => Self::Unknown(other.to_string()),
1744 }
1745 }
1746}
1747
1748impl Serialize for TaskStatus {
1749 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1750 serializer.serialize_str(self.as_str())
1751 }
1752}
1753
1754impl<'de> Deserialize<'de> for TaskStatus {
1755 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1756 let s = String::deserialize(deserializer)?;
1757 Ok(Self::from(s.as_str()))
1758 }
1759}
1760
1761#[derive(Debug, Clone, Serialize, Deserialize)]
1763pub struct TaskStartedMessage {
1764 pub session_id: String,
1765 pub task_id: String,
1766 #[serde(default, skip_serializing_if = "Option::is_none")]
1767 pub task_type: Option<TaskType>,
1768 #[serde(default, skip_serializing_if = "Option::is_none")]
1769 pub tool_use_id: Option<String>,
1770 pub description: String,
1771 #[serde(default, skip_serializing_if = "Option::is_none")]
1774 pub subagent_type: Option<String>,
1775 #[serde(default, skip_serializing_if = "Option::is_none")]
1777 pub prompt: Option<String>,
1778 #[serde(default, skip_serializing_if = "Option::is_none")]
1779 pub workflow_name: Option<String>,
1780 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 pub skip_transcript: Option<bool>,
1782 pub uuid: String,
1783}
1784
1785#[derive(Debug, Clone, Serialize, Deserialize)]
1789pub struct TaskUpdatedMessage {
1790 pub session_id: String,
1791 pub task_id: String,
1792 pub patch: TaskPatch,
1793 pub uuid: String,
1794}
1795
1796#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1799pub struct TaskPatch {
1800 #[serde(default, skip_serializing_if = "Option::is_none")]
1801 pub status: Option<TaskStatus>,
1802 #[serde(default, skip_serializing_if = "Option::is_none")]
1805 pub end_time: Option<u64>,
1806 #[serde(default, skip_serializing_if = "Option::is_none")]
1807 pub description: Option<String>,
1808 #[serde(default, skip_serializing_if = "Option::is_none")]
1809 pub total_paused_ms: Option<u64>,
1810 #[serde(default, skip_serializing_if = "Option::is_none")]
1811 pub error: Option<String>,
1812 #[serde(default, skip_serializing_if = "Option::is_none")]
1813 pub is_backgrounded: Option<bool>,
1814}
1815
1816#[derive(Debug, Clone, Serialize, Deserialize)]
1819pub struct ThinkingTokensMessage {
1820 pub session_id: String,
1821 pub estimated_tokens: u64,
1823 pub estimated_tokens_delta: u64,
1825 pub uuid: String,
1826}
1827
1828#[derive(Debug, Clone, Serialize, Deserialize)]
1831pub struct TaskProgressMessage {
1832 pub session_id: String,
1833 pub task_id: String,
1834 #[serde(default, skip_serializing_if = "Option::is_none")]
1835 pub tool_use_id: Option<String>,
1836 pub description: String,
1837 #[serde(default, skip_serializing_if = "Option::is_none")]
1838 pub last_tool_name: Option<String>,
1839 pub usage: TaskUsage,
1840 #[serde(default, skip_serializing_if = "Option::is_none")]
1842 pub subagent_type: Option<String>,
1843 #[serde(default, skip_serializing_if = "Option::is_none")]
1844 pub summary: Option<String>,
1845 pub uuid: String,
1846}
1847
1848#[derive(Debug, Clone, Serialize, Deserialize)]
1851pub struct TaskNotificationMessage {
1852 pub session_id: String,
1853 pub task_id: String,
1854 pub status: TaskStatus,
1855 pub summary: String,
1856 pub output_file: Option<String>,
1857 #[serde(skip_serializing_if = "Option::is_none")]
1858 pub tool_use_id: Option<String>,
1859 #[serde(skip_serializing_if = "Option::is_none")]
1860 pub usage: Option<TaskUsage>,
1861 #[serde(default, skip_serializing_if = "Option::is_none")]
1862 pub skip_transcript: Option<bool>,
1863 #[serde(skip_serializing_if = "Option::is_none")]
1864 pub uuid: Option<String>,
1865}
1866
1867#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1869pub enum AssistantErrorKind {
1870 AuthenticationFailed,
1871 OauthOrgNotAllowed,
1872 BillingError,
1873 RateLimit,
1874 Overloaded,
1875 InvalidRequest,
1876 ModelNotFound,
1877 ServerError,
1878 UnknownError,
1879 MaxOutputTokens,
1880 Unknown(String),
1881}
1882
1883impl AssistantErrorKind {
1884 pub fn as_str(&self) -> &str {
1885 match self {
1886 Self::AuthenticationFailed => "authentication_failed",
1887 Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
1888 Self::BillingError => "billing_error",
1889 Self::RateLimit => "rate_limit",
1890 Self::Overloaded => "overloaded",
1891 Self::InvalidRequest => "invalid_request",
1892 Self::ModelNotFound => "model_not_found",
1893 Self::ServerError => "server_error",
1894 Self::UnknownError => "unknown",
1895 Self::MaxOutputTokens => "max_output_tokens",
1896 Self::Unknown(s) => s.as_str(),
1897 }
1898 }
1899}
1900
1901impl fmt::Display for AssistantErrorKind {
1902 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1903 f.write_str(self.as_str())
1904 }
1905}
1906
1907impl From<&str> for AssistantErrorKind {
1908 fn from(s: &str) -> Self {
1909 match s {
1910 "authentication_failed" => Self::AuthenticationFailed,
1911 "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
1912 "billing_error" => Self::BillingError,
1913 "rate_limit" => Self::RateLimit,
1914 "overloaded" => Self::Overloaded,
1915 "invalid_request" => Self::InvalidRequest,
1916 "model_not_found" => Self::ModelNotFound,
1917 "server_error" => Self::ServerError,
1918 "unknown" => Self::UnknownError,
1919 "max_output_tokens" => Self::MaxOutputTokens,
1920 other => Self::Unknown(other.to_string()),
1921 }
1922 }
1923}
1924
1925impl Serialize for AssistantErrorKind {
1926 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1927 serializer.serialize_str(self.as_str())
1928 }
1929}
1930
1931impl<'de> Deserialize<'de> for AssistantErrorKind {
1932 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1933 let s = String::deserialize(deserializer)?;
1934 Ok(Self::from(s.as_str()))
1935 }
1936}
1937
1938#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1940pub struct ToolUseMeta {
1941 pub id: String,
1942 pub display_name: String,
1943 #[serde(default, skip_serializing_if = "Option::is_none")]
1944 pub server_display_name: Option<String>,
1945 #[serde(default, skip_serializing_if = "Option::is_none")]
1946 pub icon_url: Option<String>,
1947}
1948
1949#[derive(Debug, Clone, Serialize, Deserialize)]
1951pub struct AssistantMessage {
1952 pub message: AssistantMessageContent,
1953 #[serde(alias = "sessionId")]
1954 pub session_id: String,
1955 #[serde(skip_serializing_if = "Option::is_none")]
1956 pub uuid: Option<String>,
1957 #[serde(skip_serializing_if = "Option::is_none")]
1958 pub parent_tool_use_id: Option<String>,
1959 #[serde(skip_serializing_if = "Option::is_none")]
1961 pub request_id: Option<String>,
1962 #[serde(skip_serializing_if = "Option::is_none")]
1965 pub subagent_type: Option<String>,
1966 #[serde(skip_serializing_if = "Option::is_none")]
1968 pub task_description: Option<String>,
1969 #[serde(skip_serializing_if = "Option::is_none")]
1970 pub error: Option<AssistantErrorKind>,
1971 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1972 pub supersedes: Vec<String>,
1973 #[serde(skip_serializing_if = "Option::is_none")]
1974 pub timestamp: Option<String>,
1975 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1976 pub tool_use_meta: Vec<ToolUseMeta>,
1977 #[serde(default, skip_serializing_if = "Option::is_none")]
1978 pub is_meta: Option<bool>,
1979 #[serde(default, skip_serializing_if = "Option::is_none")]
1980 pub is_virtual: Option<bool>,
1981 #[serde(default, skip_serializing_if = "Option::is_none")]
1982 pub is_api_error_message: Option<bool>,
1983 #[serde(skip_serializing_if = "Option::is_none")]
1984 pub api_error_status: Option<u16>,
1985 #[serde(skip_serializing_if = "Option::is_none")]
1986 pub api_error: Option<String>,
1987 #[serde(skip_serializing_if = "Option::is_none")]
1988 pub error_details: Option<String>,
1989 #[serde(skip_serializing_if = "Option::is_none")]
1990 pub advisor_model: Option<String>,
1991 #[serde(skip_serializing_if = "Option::is_none")]
1992 pub attribution_agent: Option<String>,
1993 #[serde(skip_serializing_if = "Option::is_none")]
1994 pub attribution_skill: Option<String>,
1995 #[serde(skip_serializing_if = "Option::is_none")]
1996 pub attribution_plugin: Option<String>,
1997 #[serde(skip_serializing_if = "Option::is_none")]
1998 pub attribution_mcp_server: Option<String>,
1999 #[serde(skip_serializing_if = "Option::is_none")]
2000 pub attribution_mcp_tool: Option<String>,
2001}
2002
2003#[derive(Debug, Clone, Serialize, Deserialize)]
2005pub struct AssistantMessageContent {
2006 pub id: String,
2007 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2009 pub message_type: Option<String>,
2010 pub role: MessageRole,
2011 pub model: String,
2012 pub content: Vec<ContentBlock>,
2013 #[serde(skip_serializing_if = "Option::is_none")]
2014 pub stop_reason: Option<StopReason>,
2015 #[serde(skip_serializing_if = "Option::is_none")]
2016 pub stop_sequence: Option<String>,
2017 #[serde(skip_serializing_if = "Option::is_none")]
2018 pub usage: Option<AssistantUsage>,
2019 #[serde(skip_serializing_if = "Option::is_none")]
2021 pub stop_details: Option<Value>,
2022 #[serde(skip_serializing_if = "Option::is_none")]
2024 pub context_management: Option<Value>,
2025}
2026
2027#[derive(Debug, Clone, Serialize, Deserialize)]
2029pub struct AssistantUsage {
2030 #[serde(default)]
2032 pub input_tokens: u32,
2033
2034 #[serde(default)]
2036 pub output_tokens: u32,
2037
2038 #[serde(default)]
2040 pub cache_creation_input_tokens: u32,
2041
2042 #[serde(default)]
2044 pub cache_read_input_tokens: u32,
2045
2046 #[serde(skip_serializing_if = "Option::is_none")]
2048 pub service_tier: Option<String>,
2049
2050 #[serde(skip_serializing_if = "Option::is_none")]
2052 pub cache_creation: Option<CacheCreationDetails>,
2053
2054 #[serde(skip_serializing_if = "Option::is_none")]
2056 pub inference_geo: Option<String>,
2057}
2058
2059#[derive(Debug, Clone, Serialize, Deserialize)]
2061pub struct CacheCreationDetails {
2062 #[serde(default)]
2064 pub ephemeral_1h_input_tokens: u32,
2065
2066 #[serde(default)]
2068 pub ephemeral_5m_input_tokens: u32,
2069}
2070
2071#[cfg(test)]
2072mod tests {
2073 use crate::io::ClaudeOutput;
2074
2075 #[test]
2076 fn test_subagent_usage_rollup_accumulates_task_results() {
2077 use super::SubagentUsageRollup;
2078
2079 let mut rollup = SubagentUsageRollup::default();
2080
2081 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}}"#;
2082 let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2083 assert!(rollup.observe(&output));
2084 assert_eq!(rollup.subagent_tokens, 10201);
2085 assert_eq!(rollup.agent_count, 1);
2086 assert_eq!(rollup.tool_uses, 3);
2087 assert_eq!(rollup.duration_ms, 1853);
2088
2089 assert!(!rollup.observe(&output));
2091 assert_eq!(rollup.agent_count, 1);
2092 assert_eq!(rollup.subagent_tokens, 10201);
2093
2094 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}}"#;
2096 let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2097 assert!(rollup.observe(&output));
2098 assert_eq!(rollup.agent_count, 2);
2099 assert_eq!(rollup.subagent_tokens, 10701);
2100 }
2101
2102 #[test]
2103 fn test_subagent_usage_rollup_ignores_non_task_results() {
2104 use super::SubagentUsageRollup;
2105
2106 let mut rollup = SubagentUsageRollup::default();
2107
2108 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}}"#;
2111 let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2112 assert!(!rollup.observe(&output));
2113
2114 let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2116 let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2117 assert!(!rollup.observe(&output));
2118
2119 let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2121 let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2122 assert!(!rollup.observe(&output));
2123
2124 assert_eq!(rollup, SubagentUsageRollup::default());
2125 }
2126
2127 #[test]
2128 fn test_subagent_usage_rollup_over_captured_session() {
2129 use super::SubagentUsageRollup;
2130
2131 let mut rollup = SubagentUsageRollup::default();
2132 let fixture =
2133 include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2134 for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2135 if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2136 rollup.observe(&output);
2137 }
2138 }
2139 assert_eq!(rollup.agent_count, 1);
2140 assert_eq!(rollup.subagent_tokens, 10201);
2141 }
2142
2143 #[test]
2144 fn test_system_message_init() {
2145 let json = r#"{
2146 "type": "system",
2147 "subtype": "init",
2148 "session_id": "test-session-123",
2149 "cwd": "/home/user/project",
2150 "model": "claude-sonnet-4",
2151 "tools": ["Bash", "Read", "Write"],
2152 "mcp_servers": [],
2153 "slash_commands": ["compact", "cost", "review"],
2154 "agents": ["Bash", "Explore", "Plan"],
2155 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2156 "skills": [],
2157 "claude_code_version": "2.1.15",
2158 "apiKeySource": "none",
2159 "output_style": "default",
2160 "permissionMode": "default"
2161 }"#;
2162
2163 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2164 if let ClaudeOutput::System(sys) = output {
2165 assert!(sys.is_init());
2166 assert!(!sys.is_status());
2167 assert!(!sys.is_compact_boundary());
2168
2169 let init = sys.as_init().expect("Should parse as init");
2170 assert_eq!(init.session_id, "test-session-123");
2171 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2172 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2173 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2174 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2175 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2176 assert_eq!(init.plugins.len(), 1);
2177 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2178 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2179 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2180 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2181 assert_eq!(
2182 init.permission_mode,
2183 Some(super::InitPermissionMode::Default)
2184 );
2185 } else {
2186 panic!("Expected System message");
2187 }
2188 }
2189
2190 #[test]
2191 fn test_system_message_init_from_real_capture() {
2192 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2193 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2194 if let ClaudeOutput::System(sys) = output {
2195 let init = sys.as_init().expect("Should parse real init capture");
2196 assert_eq!(init.slash_commands.len(), 8);
2197 assert!(init.slash_commands.contains(&"compact".to_string()));
2198 assert!(init.slash_commands.contains(&"review".to_string()));
2199 assert_eq!(init.agents.len(), 5);
2200 assert!(init.agents.contains(&"Bash".to_string()));
2201 assert!(init.agents.contains(&"Explore".to_string()));
2202 assert_eq!(init.plugins.len(), 1);
2203 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2204 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2205 } else {
2206 panic!("Expected System message");
2207 }
2208 }
2209
2210 #[test]
2211 fn test_system_message_status() {
2212 let json = r#"{
2213 "type": "system",
2214 "subtype": "status",
2215 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2216 "status": "compacting",
2217 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2218 }"#;
2219
2220 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2221 if let ClaudeOutput::System(sys) = output {
2222 assert!(sys.is_status());
2223 assert!(!sys.is_init());
2224
2225 let status = sys.as_status().expect("Should parse as status");
2226 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2227 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
2228 assert_eq!(
2229 status.uuid,
2230 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2231 );
2232 } else {
2233 panic!("Expected System message");
2234 }
2235 }
2236
2237 #[test]
2238 fn test_system_message_status_null() {
2239 let json = r#"{
2240 "type": "system",
2241 "subtype": "status",
2242 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2243 "status": null,
2244 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2245 }"#;
2246
2247 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2248 if let ClaudeOutput::System(sys) = output {
2249 let status = sys.as_status().expect("Should parse as status");
2250 assert_eq!(status.status, None);
2251 } else {
2252 panic!("Expected System message");
2253 }
2254 }
2255
2256 #[test]
2257 fn test_system_message_task_started() {
2258 let json = r#"{
2259 "type": "system",
2260 "subtype": "task_started",
2261 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2262 "task_id": "b6daf3f",
2263 "task_type": "local_bash",
2264 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
2265 "description": "Wait for CI on PR #12",
2266 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
2267 }"#;
2268
2269 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2270 if let ClaudeOutput::System(sys) = output {
2271 assert!(sys.is_task_started());
2272 assert!(!sys.is_task_progress());
2273 assert!(!sys.is_task_notification());
2274
2275 let task = sys.as_task_started().expect("Should parse as task_started");
2276 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
2277 assert_eq!(task.task_id, "b6daf3f");
2278 assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
2279 assert_eq!(
2280 task.tool_use_id.as_deref(),
2281 Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
2282 );
2283 assert_eq!(task.description, "Wait for CI on PR #12");
2284 } else {
2285 panic!("Expected System message");
2286 }
2287 }
2288
2289 #[test]
2290 fn test_system_message_task_started_agent() {
2291 let json = r#"{
2292 "type": "system",
2293 "subtype": "task_started",
2294 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2295 "task_id": "a4a7e0906e5fc64cc",
2296 "task_type": "local_agent",
2297 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2298 "description": "Explore Scene/ArrayScene duplication",
2299 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2300 }"#;
2301
2302 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2303 if let ClaudeOutput::System(sys) = output {
2304 let task = sys.as_task_started().expect("Should parse as task_started");
2305 assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
2306 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
2307 } else {
2308 panic!("Expected System message");
2309 }
2310 }
2311
2312 #[test]
2313 fn test_system_message_task_progress() {
2314 let json = r#"{
2315 "type": "system",
2316 "subtype": "task_progress",
2317 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2318 "task_id": "a4a7e0906e5fc64cc",
2319 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2320 "description": "Reading src/jplephem/chebyshev.rs",
2321 "last_tool_name": "Read",
2322 "usage": {
2323 "duration_ms": 13996,
2324 "tool_uses": 9,
2325 "total_tokens": 38779
2326 },
2327 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2328 }"#;
2329
2330 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2331 if let ClaudeOutput::System(sys) = output {
2332 assert!(sys.is_task_progress());
2333 assert!(!sys.is_task_started());
2334
2335 let progress = sys
2336 .as_task_progress()
2337 .expect("Should parse as task_progress");
2338 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
2339 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
2340 assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
2341 assert_eq!(progress.usage.duration_ms, 13996);
2342 assert_eq!(progress.usage.tool_uses, 9);
2343 assert_eq!(progress.usage.total_tokens, 38779);
2344 } else {
2345 panic!("Expected System message");
2346 }
2347 }
2348
2349 #[test]
2350 fn test_system_message_task_notification_completed() {
2351 let json = r#"{
2352 "type": "system",
2353 "subtype": "task_notification",
2354 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2355 "task_id": "a0ba761e9dc9c316f",
2356 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
2357 "status": "completed",
2358 "summary": "Agent \"Write Hipparcos data source doc\" completed",
2359 "output_file": "",
2360 "usage": {
2361 "duration_ms": 172300,
2362 "tool_uses": 11,
2363 "total_tokens": 42005
2364 },
2365 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
2366 }"#;
2367
2368 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2369 if let ClaudeOutput::System(sys) = output {
2370 assert!(sys.is_task_notification());
2371
2372 let notif = sys
2373 .as_task_notification()
2374 .expect("Should parse as task_notification");
2375 assert_eq!(notif.status, super::TaskStatus::Completed);
2376 assert_eq!(
2377 notif.summary,
2378 "Agent \"Write Hipparcos data source doc\" completed"
2379 );
2380 assert_eq!(notif.output_file, Some("".to_string()));
2381 assert_eq!(
2382 notif.tool_use_id,
2383 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
2384 );
2385 let usage = notif.usage.expect("Should have usage");
2386 assert_eq!(usage.duration_ms, 172300);
2387 assert_eq!(usage.tool_uses, 11);
2388 assert_eq!(usage.total_tokens, 42005);
2389 } else {
2390 panic!("Expected System message");
2391 }
2392 }
2393
2394 #[test]
2395 fn test_system_message_task_notification_failed_no_usage() {
2396 let json = r#"{
2397 "type": "system",
2398 "subtype": "task_notification",
2399 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
2400 "task_id": "b98f6a3",
2401 "status": "failed",
2402 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
2403 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
2404 }"#;
2405
2406 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2407 if let ClaudeOutput::System(sys) = output {
2408 let notif = sys
2409 .as_task_notification()
2410 .expect("Should parse as task_notification");
2411 assert_eq!(notif.status, super::TaskStatus::Failed);
2412 assert!(notif.tool_use_id.is_none());
2413 assert!(notif.usage.is_none());
2414 assert_eq!(
2415 notif.output_file,
2416 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
2417 );
2418 } else {
2419 panic!("Expected System message");
2420 }
2421 }
2422
2423 #[test]
2429 fn test_task_messages_roundtrip_through_value() {
2430 let cases = [
2431 r#"{"type":"system","subtype":"task_started","session_id":"s1",
2432 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
2433 "description":"Sleep 3s","uuid":"u1"}"#,
2434 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
2435 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
2436 "last_tool_name":"Bash",
2437 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2438 "uuid":"u2"}"#,
2439 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
2440 "task_id":"t1","tool_use_id":"tu1","status":"completed",
2441 "summary":"done","output_file":"",
2442 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2443 "uuid":"u3"}"#,
2444 ];
2445
2446 for json in cases {
2447 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2448 let value = serde_json::to_value(&output).unwrap();
2449 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
2450
2451 let ClaudeOutput::System(sys) = reparsed else {
2452 panic!("Expected System variant after round-trip");
2453 };
2454
2455 match sys.subtype {
2456 super::SystemSubtype::TaskStarted => {
2457 assert!(
2458 sys.as_task_started().is_some(),
2459 "as_task_started failed after round-trip"
2460 );
2461 }
2462 super::SystemSubtype::TaskProgress => {
2463 assert!(
2464 sys.as_task_progress().is_some(),
2465 "as_task_progress failed after round-trip"
2466 );
2467 }
2468 super::SystemSubtype::TaskNotification => {
2469 assert!(
2470 sys.as_task_notification().is_some(),
2471 "as_task_notification failed after round-trip"
2472 );
2473 }
2474 other => panic!("unexpected subtype after round-trip: {other:?}"),
2475 }
2476 }
2477 }
2478
2479 #[test]
2480 fn test_system_message_compact_boundary() {
2481 let json = r#"{
2482 "type": "system",
2483 "subtype": "compact_boundary",
2484 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2485 "compact_metadata": {
2486 "pre_tokens": 155285,
2487 "trigger": "auto"
2488 },
2489 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
2490 }"#;
2491
2492 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2493 if let ClaudeOutput::System(sys) = output {
2494 assert!(sys.is_compact_boundary());
2495 assert!(!sys.is_init());
2496 assert!(!sys.is_status());
2497
2498 let compact = sys
2499 .as_compact_boundary()
2500 .expect("Should parse as compact_boundary");
2501 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2502 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
2503 assert_eq!(
2504 compact.compact_metadata.trigger,
2505 super::CompactionTrigger::Auto
2506 );
2507 assert!(compact.summary.is_none());
2509 assert!(compact.leaf_message_count.is_none());
2510 assert!(compact.duration_ms.is_none());
2511 } else {
2512 panic!("Expected System message");
2513 }
2514 }
2515
2516 #[test]
2517 fn test_compact_boundary_with_summary_stats() {
2518 let json = r#"{
2520 "type": "system",
2521 "subtype": "compact_boundary",
2522 "session_id": "s1",
2523 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
2524 "summary": "Summarized the earlier exploration.",
2525 "leaf_message_count": 42,
2526 "duration_ms": 1234,
2527 "uuid": "u1"
2528 }"#;
2529 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2530 let ClaudeOutput::System(sys) = output else {
2531 panic!("Expected System message");
2532 };
2533 let compact = sys.as_compact_boundary().expect("compact_boundary");
2534 assert_eq!(
2535 compact.summary.as_deref(),
2536 Some("Summarized the earlier exploration.")
2537 );
2538 assert_eq!(compact.leaf_message_count, Some(42));
2539 assert_eq!(compact.duration_ms, Some(1234));
2540
2541 let json_alt = r#"{
2544 "type": "system",
2545 "subtype": "compact_boundary",
2546 "session_id": "s2",
2547 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
2548 "content": "alt-key summary",
2549 "message_count": 7
2550 }"#;
2551 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
2552 let ClaudeOutput::System(sys) = output else {
2553 panic!("Expected System message");
2554 };
2555 let compact = sys.as_compact_boundary().expect("compact_boundary");
2556 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
2557 assert_eq!(compact.leaf_message_count, Some(7));
2558 }
2559
2560 #[test]
2561 fn test_init_message_with_new_fields() {
2562 let json = r#"{
2563 "type": "system",
2564 "subtype": "init",
2565 "session_id": "test-session",
2566 "cwd": "/home/user",
2567 "model": "claude-opus-4-7",
2568 "tools": ["Bash"],
2569 "mcp_servers": [],
2570 "permissionMode": "default",
2571 "apiKeySource": "none",
2572 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
2573 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
2574 "fast_mode_state": "off",
2575 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
2576 "claude_code_version": "2.1.117"
2577 }"#;
2578
2579 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2580 if let ClaudeOutput::System(sys) = output {
2581 let init = sys.as_init().expect("Should parse as init");
2582 assert_eq!(
2583 init.uuid.as_deref(),
2584 Some("44841a0d-182d-493a-86b5-79800d3d9665")
2585 );
2586 assert!(init.memory_paths.is_some());
2587 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
2588 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
2589 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
2590 } else {
2591 panic!("Expected System message");
2592 }
2593 }
2594
2595 #[test]
2596 fn test_assistant_message_with_new_fields() {
2597 let json = r#"{
2598 "type": "assistant",
2599 "message": {
2600 "id": "msg_1",
2601 "type": "message",
2602 "role": "assistant",
2603 "model": "claude-opus-4-7",
2604 "content": [{"type": "text", "text": "Hello"}],
2605 "stop_reason": "end_turn",
2606 "stop_details": null,
2607 "context_management": null,
2608 "usage": {
2609 "input_tokens": 100,
2610 "output_tokens": 10,
2611 "cache_creation_input_tokens": 50,
2612 "cache_read_input_tokens": 0,
2613 "service_tier": "standard",
2614 "inference_geo": "not_available"
2615 }
2616 },
2617 "session_id": "abc",
2618 "uuid": "msg-uuid-123"
2619 }"#;
2620
2621 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2622 if let ClaudeOutput::Assistant(asst) = output {
2623 assert_eq!(asst.message.stop_details, None);
2624 assert_eq!(asst.message.context_management, None);
2625 let usage = asst.message.usage.unwrap();
2626 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
2627 } else {
2628 panic!("Expected Assistant message");
2629 }
2630 }
2631
2632 #[test]
2633 fn test_user_message_with_new_fields() {
2634 let json = r#"{
2635 "type": "user",
2636 "message": {
2637 "role": "user",
2638 "content": [{"type": "text", "text": "Hello"}]
2639 },
2640 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2641 "parent_tool_use_id": "toolu_123",
2642 "uuid": "user-msg-456"
2643 }"#;
2644
2645 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2646 if let ClaudeOutput::User(user) = output {
2647 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
2648 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
2649 } else {
2650 panic!("Expected User message");
2651 }
2652 }
2653
2654 #[test]
2660 fn test_user_message_preserves_tool_use_result_and_timestamp() {
2661 let json = r#"{
2662 "type":"user",
2663 "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"}]},
2664 "parent_tool_use_id":null,
2665 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
2666 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
2667 "timestamp":"2026-05-12T23:12:04.121Z",
2668 "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"}}
2669 }"#;
2670
2671 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2672 let user = match output {
2673 ClaudeOutput::User(u) => u,
2674 other => panic!("Expected User message, got {:?}", other.message_type()),
2675 };
2676
2677 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
2678 let raw = user
2679 .tool_use_result
2680 .as_ref()
2681 .expect("tool_use_result must be captured");
2682 assert_eq!(raw["answers"]["Color"], "Blue");
2683 assert_eq!(raw["questions"][0]["header"], "Color");
2684
2685 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
2689 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
2690 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
2691 assert_eq!(
2692 reser["tool_use_result"]["questions"][0]["question"],
2693 "Which color do you prefer?"
2694 );
2695
2696 let typed: crate::AskUserQuestionInput = user
2699 .tool_use_result_as::<crate::AskUserQuestionInput>()
2700 .expect("tool_use_result present")
2701 .expect("AskUserQuestionInput parses");
2702 assert_eq!(typed.questions.len(), 1);
2703 assert_eq!(typed.questions[0].header, "Color");
2704 let answers = typed.answers.expect("answers populated");
2705 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
2706 }
2707
2708 #[test]
2711 fn test_user_message_without_tool_use_result_omits_field() {
2712 let json = r#"{
2713 "type":"user",
2714 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
2715 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
2716 }"#;
2717
2718 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2719 let user = match output {
2720 ClaudeOutput::User(u) => u,
2721 _ => panic!("Expected User message"),
2722 };
2723 assert!(user.tool_use_result.is_none());
2724 assert!(user.timestamp.is_none());
2725
2726 let reser = serde_json::to_value(&user).unwrap();
2727 assert!(reser.get("tool_use_result").is_none());
2728 assert!(reser.get("timestamp").is_none());
2729 }
2730
2731 #[test]
2735 fn test_subagent_result_exposes_token_accounting() {
2736 let json = r#"{
2737 "type":"user",
2738 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
2739 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
2740 "tool_use_result":{
2741 "status":"completed",
2742 "prompt":"Count the .rs files.",
2743 "agentId":"ac4f0276e9d4b6232",
2744 "agentType":"Explore",
2745 "content":[{"type":"text","text":"21"}],
2746 "resolvedModel":"claude-haiku-4-5-20251001",
2747 "totalDurationMs":6869,
2748 "totalTokens":7834,
2749 "totalToolUseCount":1,
2750 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
2751 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
2752 }
2753 }"#;
2754
2755 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2756 let user = match output {
2757 ClaudeOutput::User(u) => u,
2758 _ => panic!("Expected User message"),
2759 };
2760
2761 let result = user.subagent_result().expect("subagent result parses");
2762 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
2763 assert_eq!(
2764 result.resolved_model.as_deref(),
2765 Some("claude-haiku-4-5-20251001")
2766 );
2767 assert_eq!(result.total_tokens, Some(7834));
2768 assert_eq!(result.total_duration_ms, Some(6869));
2769 assert_eq!(result.total_tool_use_count, Some(1));
2770
2771 let usage = result.usage.expect("nested usage present");
2772 assert_eq!(usage.input_tokens, 6);
2773 assert_eq!(usage.cache_read_input_tokens, 7699);
2774
2775 let stats = result.tool_stats.expect("toolStats present");
2776 assert_eq!(stats.bash_count, 1);
2777 }
2778
2779 #[test]
2783 fn test_subagent_result_absent_for_non_task_result() {
2784 let json = r#"{
2785 "type":"user",
2786 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
2787 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
2788 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
2789 }"#;
2790
2791 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2792 let user = match output {
2793 ClaudeOutput::User(u) => u,
2794 _ => panic!("Expected User message"),
2795 };
2796
2797 let result = user.subagent_result().expect("lenient parse");
2798 assert_eq!(result.total_tokens, None);
2799 assert_eq!(result.agent_type, None);
2800 }
2801}