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}
576
577#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
579pub struct ToolResultMeta {
580 pub id: String,
582 pub non_execution_kind: String,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
590 pub user_feedback: Option<String>,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct UserMessage {
596 pub message: MessageContent,
597 #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
598 #[serde(
599 serialize_with = "serialize_optional_uuid",
600 deserialize_with = "deserialize_optional_uuid"
601 )]
602 pub session_id: Option<Uuid>,
603 #[serde(skip_serializing_if = "Option::is_none")]
605 pub parent_tool_use_id: Option<String>,
606 #[serde(skip_serializing_if = "Option::is_none")]
608 pub uuid: Option<String>,
609 #[serde(skip_serializing_if = "Option::is_none")]
611 pub timestamp: Option<String>,
612 #[serde(skip_serializing_if = "Option::is_none")]
619 pub tool_use_result: Option<serde_json::Value>,
620 #[serde(skip_serializing_if = "Option::is_none")]
623 pub subagent_type: Option<String>,
624 #[serde(skip_serializing_if = "Option::is_none")]
626 pub task_description: Option<String>,
627 #[serde(skip_serializing_if = "Option::is_none")]
628 pub origin: Option<MessageOrigin>,
629 #[serde(skip_serializing_if = "Option::is_none")]
630 pub priority: Option<String>,
631 #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
632 pub is_synthetic: Option<bool>,
633 #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
634 pub should_query: Option<bool>,
635 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub is_meta: Option<bool>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub is_visible_in_transcript_only: Option<bool>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub is_virtual: Option<bool>,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub is_compact_summary: Option<bool>,
643 #[serde(skip_serializing_if = "Option::is_none")]
644 pub summarize_metadata: Option<SummarizeMetadata>,
645 #[serde(skip_serializing_if = "Option::is_none")]
646 pub mcp_meta: Option<McpMeta>,
647 #[serde(skip_serializing_if = "Option::is_none")]
650 pub tool_result_meta: Option<Vec<ToolResultMeta>>,
651 #[serde(skip_serializing_if = "Option::is_none")]
652 pub source_tool_use_id: Option<String>,
653 #[serde(skip_serializing_if = "Option::is_none")]
654 pub source_tool_assistant_uuid: Option<String>,
655 #[serde(skip_serializing_if = "Option::is_none")]
656 pub image_paste_ids: Option<Vec<u64>>,
657 #[serde(skip_serializing_if = "Option::is_none")]
658 pub client_platform: Option<String>,
659 #[serde(skip_serializing_if = "Option::is_none")]
660 pub inbound_origin: Option<String>,
661 #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
662 pub is_replay: Option<bool>,
663 #[serde(skip_serializing_if = "Option::is_none")]
664 pub file_attachments: Option<Vec<Value>>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub seeded_summon: Option<bool>,
668}
669
670impl UserMessage {
671 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
679 &self,
680 ) -> Option<Result<T, serde_json::Error>> {
681 self.tool_use_result
682 .as_ref()
683 .map(|v| serde_json::from_value(v.clone()))
684 }
685
686 pub fn subagent_result(&self) -> Option<SubagentResult> {
697 self.tool_use_result
698 .as_ref()
699 .and_then(|v| serde_json::from_value(v.clone()).ok())
700 }
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize)]
714pub struct SubagentResult {
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub status: Option<String>,
718 #[serde(skip_serializing_if = "Option::is_none")]
720 pub prompt: Option<String>,
721 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
723 pub agent_id: Option<String>,
724 #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
726 pub agent_type: Option<String>,
727 #[serde(
729 default,
730 deserialize_with = "deserialize_content_blocks",
731 skip_serializing_if = "Vec::is_empty"
732 )]
733 pub content: Vec<ContentBlock>,
734 #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
736 pub resolved_model: Option<String>,
737 #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
739 pub total_duration_ms: Option<u64>,
740 #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
742 pub total_tokens: Option<u64>,
743 #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
745 pub total_tool_use_count: Option<u64>,
746 #[serde(skip_serializing_if = "Option::is_none")]
748 pub usage: Option<super::result::UsageInfo>,
749 #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
751 pub tool_stats: Option<SubagentToolStats>,
752}
753
754#[derive(Debug, Clone, Default, Serialize, Deserialize)]
759#[serde(rename_all = "camelCase")]
760pub struct SubagentToolStats {
761 #[serde(default)]
762 pub read_count: u64,
763 #[serde(default)]
764 pub search_count: u64,
765 #[serde(default)]
766 pub bash_count: u64,
767 #[serde(default)]
768 pub edit_file_count: u64,
769 #[serde(default)]
770 pub lines_added: u64,
771 #[serde(default)]
772 pub lines_removed: u64,
773 #[serde(default)]
774 pub other_tool_count: u64,
775 #[serde(flatten)]
776 pub extra: serde_json::Map<String, Value>,
777}
778
779#[derive(Debug, Clone, Default, PartialEq, Eq)]
806pub struct SubagentUsageRollup {
807 pub subagent_tokens: u64,
810 pub agent_count: u32,
812 pub tool_uses: u64,
814 pub duration_ms: u64,
816 seen_agent_ids: std::collections::BTreeSet<String>,
817}
818
819impl SubagentUsageRollup {
820 pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
826 match output {
827 ClaudeOutput::User(user) => self.observe_user(user),
828 _ => false,
829 }
830 }
831
832 pub fn observe_user(&mut self, user: &UserMessage) -> bool {
839 let Some(result) = user.subagent_result() else {
840 return false;
841 };
842 if result.total_tokens.is_none() && result.agent_id.is_none() {
843 return false;
844 }
845 if let Some(agent_id) = &result.agent_id {
846 if !self.seen_agent_ids.insert(agent_id.clone()) {
847 return false;
848 }
849 }
850 self.agent_count += 1;
851 self.subagent_tokens += result.total_tokens.unwrap_or(0);
852 self.tool_uses += result.total_tool_use_count.unwrap_or(0);
853 self.duration_ms += result.total_duration_ms.unwrap_or(0);
854 true
855 }
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct MessageContent {
861 pub role: MessageRole,
862 #[serde(deserialize_with = "deserialize_content_blocks")]
863 pub content: Vec<ContentBlock>,
864}
865
866#[derive(Debug, Clone, Serialize, Deserialize)]
868pub struct SystemMessage {
869 pub subtype: SystemSubtype,
870 #[serde(flatten)]
871 pub data: Value, }
873
874impl SystemMessage {
875 pub fn is_init(&self) -> bool {
877 self.subtype == SystemSubtype::Init
878 }
879
880 pub fn is_status(&self) -> bool {
882 self.subtype == SystemSubtype::Status
883 }
884
885 pub fn is_compact_boundary(&self) -> bool {
887 self.subtype == SystemSubtype::CompactBoundary
888 }
889
890 pub fn as_init(&self) -> Option<InitMessage> {
892 if self.subtype != SystemSubtype::Init {
893 return None;
894 }
895 serde_json::from_value(self.data.clone()).ok()
896 }
897
898 pub fn as_status(&self) -> Option<StatusMessage> {
900 if self.subtype != SystemSubtype::Status {
901 return None;
902 }
903 serde_json::from_value(self.data.clone()).ok()
904 }
905
906 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
908 if self.subtype != SystemSubtype::CompactBoundary {
909 return None;
910 }
911 serde_json::from_value(self.data.clone()).ok()
912 }
913
914 pub fn is_task_started(&self) -> bool {
916 self.subtype == SystemSubtype::TaskStarted
917 }
918
919 pub fn is_task_progress(&self) -> bool {
921 self.subtype == SystemSubtype::TaskProgress
922 }
923
924 pub fn is_task_notification(&self) -> bool {
926 self.subtype == SystemSubtype::TaskNotification
927 }
928
929 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
931 if self.subtype != SystemSubtype::TaskStarted {
932 return None;
933 }
934 serde_json::from_value(self.data.clone()).ok()
935 }
936
937 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
939 if self.subtype != SystemSubtype::TaskProgress {
940 return None;
941 }
942 serde_json::from_value(self.data.clone()).ok()
943 }
944
945 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
947 if self.subtype != SystemSubtype::TaskNotification {
948 return None;
949 }
950 serde_json::from_value(self.data.clone()).ok()
951 }
952
953 pub fn is_task_updated(&self) -> bool {
955 self.subtype == SystemSubtype::TaskUpdated
956 }
957
958 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
960 if self.subtype != SystemSubtype::TaskUpdated {
961 return None;
962 }
963 serde_json::from_value(self.data.clone()).ok()
964 }
965
966 pub fn is_thinking_tokens(&self) -> bool {
968 self.subtype == SystemSubtype::ThinkingTokens
969 }
970
971 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
973 if self.subtype != SystemSubtype::ThinkingTokens {
974 return None;
975 }
976 serde_json::from_value(self.data.clone()).ok()
977 }
978
979 pub fn is_code_change_published(&self) -> bool {
981 self.subtype == SystemSubtype::CodeChangePublished
982 }
983
984 pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
986 if self.subtype != SystemSubtype::CodeChangePublished {
987 return None;
988 }
989 serde_json::from_value(self.data.clone()).ok()
990 }
991
992 pub fn is_vcs_state_changed(&self) -> bool {
994 self.subtype == SystemSubtype::VcsStateChanged
995 }
996
997 pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
999 if self.subtype != SystemSubtype::VcsStateChanged {
1000 return None;
1001 }
1002 serde_json::from_value(self.data.clone()).ok()
1003 }
1004
1005 pub fn is_feedback_draft_queued(&self) -> bool {
1007 self.subtype == SystemSubtype::FeedbackDraftQueued
1008 }
1009
1010 pub fn as_feedback_draft_queued(&self) -> Option<FeedbackDraftQueuedMessage> {
1012 if self.subtype != SystemSubtype::FeedbackDraftQueued {
1013 return None;
1014 }
1015 serde_json::from_value(self.data.clone()).ok()
1016 }
1017
1018 pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
1020 macro_rules! parse {
1021 ($variant:ident, $ty:ty) => {
1022 serde_json::from_value::<$ty>(self.data.clone())
1023 .ok()
1024 .map(KnownSystemEvent::$variant)
1025 };
1026 }
1027
1028 match self.subtype {
1029 SystemSubtype::Init => parse!(Init, InitMessage),
1030 SystemSubtype::Status => parse!(Status, StatusMessage),
1031 SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
1032 SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
1033 SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
1034 SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
1035 SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
1036 SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
1037 SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
1038 SystemSubtype::ControlRequestProgress => {
1039 parse!(ControlRequestProgress, ControlRequestProgressMessage)
1040 }
1041 SystemSubtype::ModelRefusalFallback => {
1042 parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
1043 }
1044 SystemSubtype::ModelRefusalNoFallback => {
1045 parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
1046 }
1047 SystemSubtype::LocalCommandOutput => {
1048 parse!(LocalCommandOutput, LocalCommandOutputMessage)
1049 }
1050 SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
1051 SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
1052 SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
1053 SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
1054 SystemSubtype::BackgroundTasksChanged => {
1055 parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
1056 }
1057 SystemSubtype::SessionStateChanged => {
1058 parse!(SessionStateChanged, SessionStateChangedMessage)
1059 }
1060 SystemSubtype::WorkerShuttingDown => {
1061 parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
1062 }
1063 SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
1064 SystemSubtype::Notification => parse!(Notification, NotificationMessage),
1065 SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
1066 SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
1067 SystemSubtype::ElicitationComplete => {
1068 parse!(ElicitationComplete, ElicitationCompleteMessage)
1069 }
1070 SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1071 SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1072 SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1073 SystemSubtype::CodeChangePublished => {
1074 parse!(CodeChangePublished, CodeChangePublishedMessage)
1075 }
1076 SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
1077 SystemSubtype::FeedbackDraftQueued => {
1078 parse!(FeedbackDraftQueued, FeedbackDraftQueuedMessage)
1079 }
1080 SystemSubtype::Unknown(_) => None,
1081 }
1082 }
1083
1084 pub fn typed_value(&self) -> Option<Value> {
1093 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1094 parsed.and_then(|v| serde_json::to_value(v).ok())
1095 }
1096 match self.subtype {
1097 SystemSubtype::Init => reserialize(self.as_init()),
1098 SystemSubtype::Status => reserialize(self.as_status()),
1099 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1100 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1101 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1102 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1103 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1104 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1105 SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1106 SystemSubtype::ControlRequestProgress => {
1107 reserialize(parse_system::<ControlRequestProgressMessage>(self))
1108 }
1109 SystemSubtype::ModelRefusalFallback => {
1110 reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1111 }
1112 SystemSubtype::ModelRefusalNoFallback => {
1113 reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1114 }
1115 SystemSubtype::LocalCommandOutput => {
1116 reserialize(parse_system::<LocalCommandOutputMessage>(self))
1117 }
1118 SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1119 SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1120 SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1121 SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1122 SystemSubtype::BackgroundTasksChanged => {
1123 reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1124 }
1125 SystemSubtype::SessionStateChanged => {
1126 reserialize(parse_system::<SessionStateChangedMessage>(self))
1127 }
1128 SystemSubtype::WorkerShuttingDown => {
1129 reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1130 }
1131 SystemSubtype::CommandsChanged => {
1132 reserialize(parse_system::<CommandsChangedMessage>(self))
1133 }
1134 SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1135 SystemSubtype::FilesPersisted => {
1136 reserialize(parse_system::<FilesPersistedMessage>(self))
1137 }
1138 SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1139 SystemSubtype::ElicitationComplete => {
1140 reserialize(parse_system::<ElicitationCompleteMessage>(self))
1141 }
1142 SystemSubtype::PermissionDenied => {
1143 reserialize(parse_system::<PermissionDeniedMessage>(self))
1144 }
1145 SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1146 SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1147 SystemSubtype::CodeChangePublished => {
1148 reserialize(parse_system::<CodeChangePublishedMessage>(self))
1149 }
1150 SystemSubtype::VcsStateChanged => {
1151 reserialize(parse_system::<VcsStateChangedMessage>(self))
1152 }
1153 SystemSubtype::FeedbackDraftQueued => {
1154 reserialize(parse_system::<FeedbackDraftQueuedMessage>(self))
1155 }
1156 SystemSubtype::Unknown(_) => None,
1157 }
1158 }
1159}
1160
1161fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1162 serde_json::from_value(message.data.clone()).ok()
1163}
1164
1165#[allow(clippy::large_enum_variant)]
1171#[derive(Debug, Clone, Serialize, Deserialize)]
1172pub enum KnownSystemEvent {
1173 Init(InitMessage),
1174 Status(StatusMessage),
1175 CompactBoundary(CompactBoundaryMessage),
1176 ThinkingTokens(ThinkingTokensMessage),
1177 TaskStarted(TaskStartedMessage),
1178 TaskProgress(TaskProgressMessage),
1179 TaskUpdated(TaskUpdatedMessage),
1180 TaskNotification(TaskNotificationMessage),
1181 ApiRetry(ApiRetryMessage),
1182 ControlRequestProgress(ControlRequestProgressMessage),
1183 ModelRefusalFallback(ModelRefusalFallbackMessage),
1184 ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1185 LocalCommandOutput(LocalCommandOutputMessage),
1186 HookStarted(HookStartedMessage),
1187 HookProgress(HookProgressMessage),
1188 HookResponse(HookResponseMessage),
1189 PluginInstall(PluginInstallMessage),
1190 BackgroundTasksChanged(BackgroundTasksChangedMessage),
1191 SessionStateChanged(SessionStateChangedMessage),
1192 WorkerShuttingDown(WorkerShuttingDownMessage),
1193 CommandsChanged(CommandsChangedMessage),
1194 Notification(NotificationMessage),
1195 FilesPersisted(FilesPersistedMessage),
1196 MemoryRecall(MemoryRecallMessage),
1197 ElicitationComplete(ElicitationCompleteMessage),
1198 PermissionDenied(PermissionDeniedMessage),
1199 MirrorError(MirrorErrorMessage),
1200 Informational(InformationalMessage),
1201 CodeChangePublished(CodeChangePublishedMessage),
1202 VcsStateChanged(VcsStateChangedMessage),
1203 FeedbackDraftQueued(FeedbackDraftQueuedMessage),
1204}
1205
1206#[derive(Debug, Clone, Serialize, Deserialize)]
1207pub struct ApiRetryMessage {
1208 pub attempt: u64,
1209 pub max_retries: u64,
1210 pub retry_delay_ms: u64,
1211 pub error_status: Option<u16>,
1212 pub error: String,
1213 #[serde(default, skip_serializing_if = "Option::is_none")]
1214 pub uuid: Option<String>,
1215 #[serde(default, skip_serializing_if = "Option::is_none")]
1216 pub session_id: Option<String>,
1217}
1218
1219#[derive(Debug, Clone, Serialize, Deserialize)]
1220pub struct ControlRequestProgressMessage {
1221 pub request_id: String,
1222 pub status: String,
1223 #[serde(default, skip_serializing_if = "Option::is_none")]
1224 pub attempt: Option<u64>,
1225 #[serde(default, skip_serializing_if = "Option::is_none")]
1226 pub max_retries: Option<u64>,
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub retry_delay_ms: Option<u64>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub error_status: Option<u16>,
1231 #[serde(default, skip_serializing_if = "Option::is_none")]
1232 pub error: Option<String>,
1233 #[serde(default, skip_serializing_if = "Option::is_none")]
1234 pub uuid: Option<String>,
1235 #[serde(default, skip_serializing_if = "Option::is_none")]
1236 pub session_id: Option<String>,
1237}
1238
1239#[derive(Debug, Clone, Serialize, Deserialize)]
1240pub struct ModelRefusalFallbackMessage {
1241 pub trigger: String,
1242 pub direction: String,
1243 #[serde(default, skip_serializing_if = "Option::is_none")]
1249 pub scope: Option<RefusalFallbackScope>,
1250 pub original_model: String,
1251 pub fallback_model: String,
1252 pub request_id: Option<String>,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub api_refusal_category: Option<String>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub saw_cyber_refusal: Option<bool>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub api_refusal_explanation: Option<String>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1264 pub retracted_message_uuids: Option<Vec<String>>,
1265 #[serde(default, skip_serializing_if = "Option::is_none")]
1266 pub refused_user_message_uuid: Option<String>,
1267 pub content: Value,
1268 #[serde(default, skip_serializing_if = "Option::is_none")]
1269 pub uuid: Option<String>,
1270 #[serde(default, skip_serializing_if = "Option::is_none")]
1271 pub session_id: Option<String>,
1272}
1273
1274#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1278pub enum RefusalFallbackScope {
1279 Session,
1281 Local,
1284 Unknown(String),
1286}
1287
1288impl RefusalFallbackScope {
1289 pub fn as_str(&self) -> &str {
1290 match self {
1291 Self::Session => "session",
1292 Self::Local => "local",
1293 Self::Unknown(s) => s.as_str(),
1294 }
1295 }
1296}
1297
1298impl fmt::Display for RefusalFallbackScope {
1299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1300 f.write_str(self.as_str())
1301 }
1302}
1303
1304impl From<&str> for RefusalFallbackScope {
1305 fn from(s: &str) -> Self {
1306 match s {
1307 "session" => Self::Session,
1308 "local" => Self::Local,
1309 other => Self::Unknown(other.to_string()),
1310 }
1311 }
1312}
1313
1314impl Serialize for RefusalFallbackScope {
1315 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1316 serializer.serialize_str(self.as_str())
1317 }
1318}
1319
1320impl<'de> Deserialize<'de> for RefusalFallbackScope {
1321 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1322 let s = String::deserialize(deserializer)?;
1323 Ok(Self::from(s.as_str()))
1324 }
1325}
1326
1327#[derive(Debug, Clone, Serialize, Deserialize)]
1328pub struct ModelRefusalNoFallbackMessage {
1329 pub original_model: String,
1330 pub request_id: Option<String>,
1331 #[serde(default, skip_serializing_if = "Option::is_none")]
1332 pub api_refusal_category: Option<String>,
1333 #[serde(default, skip_serializing_if = "Option::is_none")]
1334 pub api_refusal_explanation: Option<String>,
1335 pub content: Value,
1336 #[serde(default, skip_serializing_if = "Option::is_none")]
1337 pub uuid: Option<String>,
1338 #[serde(default, skip_serializing_if = "Option::is_none")]
1339 pub session_id: Option<String>,
1340}
1341
1342#[derive(Debug, Clone, Serialize, Deserialize)]
1343pub struct LocalCommandOutputMessage {
1344 pub content: String,
1345 #[serde(default, skip_serializing_if = "Option::is_none")]
1346 pub uuid: Option<String>,
1347 #[serde(default, skip_serializing_if = "Option::is_none")]
1348 pub session_id: Option<String>,
1349}
1350
1351#[derive(Debug, Clone, Serialize, Deserialize)]
1352pub struct HookStartedMessage {
1353 pub hook_id: String,
1354 pub hook_name: String,
1355 pub hook_event: String,
1356 #[serde(default, skip_serializing_if = "Option::is_none")]
1357 pub uuid: Option<String>,
1358 #[serde(default, skip_serializing_if = "Option::is_none")]
1359 pub session_id: Option<String>,
1360}
1361
1362#[derive(Debug, Clone, Serialize, Deserialize)]
1363pub struct HookProgressMessage {
1364 pub hook_id: String,
1365 pub hook_name: String,
1366 pub hook_event: String,
1367 #[serde(default, skip_serializing_if = "Option::is_none")]
1368 pub stdout: Option<String>,
1369 #[serde(default, skip_serializing_if = "Option::is_none")]
1370 pub stderr: Option<String>,
1371 #[serde(default, skip_serializing_if = "Option::is_none")]
1372 pub output: Option<String>,
1373 #[serde(default, skip_serializing_if = "Option::is_none")]
1374 pub uuid: Option<String>,
1375 #[serde(default, skip_serializing_if = "Option::is_none")]
1376 pub session_id: Option<String>,
1377}
1378
1379#[derive(Debug, Clone, Serialize, Deserialize)]
1380pub struct HookResponseMessage {
1381 pub hook_id: String,
1382 pub hook_name: String,
1383 pub hook_event: String,
1384 #[serde(default, skip_serializing_if = "Option::is_none")]
1385 pub stdout: Option<String>,
1386 #[serde(default, skip_serializing_if = "Option::is_none")]
1387 pub stderr: Option<String>,
1388 #[serde(default, skip_serializing_if = "Option::is_none")]
1389 pub output: Option<String>,
1390 #[serde(default, skip_serializing_if = "Option::is_none")]
1391 pub exit_code: Option<i32>,
1392 pub outcome: String,
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1394 pub uuid: Option<String>,
1395 #[serde(default, skip_serializing_if = "Option::is_none")]
1396 pub session_id: Option<String>,
1397}
1398
1399#[derive(Debug, Clone, Serialize, Deserialize)]
1400pub struct PluginInstallMessage {
1401 pub status: String,
1402 #[serde(default, skip_serializing_if = "Option::is_none")]
1403 pub name: Option<String>,
1404 #[serde(default, skip_serializing_if = "Option::is_none")]
1405 pub error: Option<String>,
1406 #[serde(default, skip_serializing_if = "Option::is_none")]
1407 pub uuid: Option<String>,
1408 #[serde(default, skip_serializing_if = "Option::is_none")]
1409 pub session_id: Option<String>,
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1413pub struct BackgroundTasksChangedMessage {
1414 pub tasks: Vec<BackgroundTaskInfo>,
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)]
1422pub struct BackgroundTaskInfo {
1423 pub task_id: String,
1424 pub task_type: String,
1425 pub description: String,
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct SessionStateChangedMessage {
1430 pub state: String,
1431 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 pub uuid: Option<String>,
1433 #[serde(default, skip_serializing_if = "Option::is_none")]
1434 pub session_id: Option<String>,
1435}
1436
1437#[derive(Debug, Clone, Serialize, Deserialize)]
1438pub struct WorkerShuttingDownMessage {
1439 pub reason: String,
1440 #[serde(default, skip_serializing_if = "Option::is_none")]
1441 pub uuid: Option<String>,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 pub session_id: Option<String>,
1444}
1445
1446#[derive(Debug, Clone, Serialize, Deserialize)]
1447pub struct CommandsChangedMessage {
1448 pub commands: Vec<CommandInfo>,
1449 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 pub uuid: Option<String>,
1451 #[serde(default, skip_serializing_if = "Option::is_none")]
1452 pub session_id: Option<String>,
1453}
1454
1455#[derive(Debug, Clone, Serialize, Deserialize)]
1456pub struct CommandInfo {
1457 pub name: String,
1458 pub description: String,
1459 #[serde(rename = "argumentHint")]
1460 pub argument_hint: String,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1462 pub aliases: Option<Vec<String>>,
1463}
1464
1465#[derive(Debug, Clone, Serialize, Deserialize)]
1466pub struct NotificationMessage {
1467 pub key: String,
1468 pub text: String,
1469 pub priority: String,
1470 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub color: Option<String>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub timeout_ms: Option<u64>,
1474 #[serde(default, skip_serializing_if = "Option::is_none")]
1475 pub uuid: Option<String>,
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1477 pub session_id: Option<String>,
1478}
1479
1480#[derive(Debug, Clone, Serialize, Deserialize)]
1481pub struct FilesPersistedMessage {
1482 pub files: Vec<PersistedFile>,
1483 pub failed: Vec<FailedPersistedFile>,
1484 pub processed_at: String,
1485 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 pub uuid: Option<String>,
1487 #[serde(default, skip_serializing_if = "Option::is_none")]
1488 pub session_id: Option<String>,
1489}
1490
1491#[derive(Debug, Clone, Serialize, Deserialize)]
1492pub struct PersistedFile {
1493 pub filename: String,
1494 pub file_id: String,
1495}
1496
1497#[derive(Debug, Clone, Serialize, Deserialize)]
1498pub struct FailedPersistedFile {
1499 pub filename: String,
1500 pub error: String,
1501}
1502
1503#[derive(Debug, Clone, Serialize, Deserialize)]
1504pub struct MemoryRecallMessage {
1505 pub mode: String,
1506 pub memories: Vec<MemoryRecallItem>,
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 MemoryRecallItem {
1515 pub path: String,
1516 pub scope: String,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1518 pub content: Option<String>,
1519}
1520
1521#[derive(Debug, Clone, Serialize, Deserialize)]
1522pub struct ElicitationCompleteMessage {
1523 pub mcp_server_name: String,
1524 pub elicitation_id: String,
1525 #[serde(default, skip_serializing_if = "Option::is_none")]
1526 pub uuid: Option<String>,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1528 pub session_id: Option<String>,
1529}
1530
1531#[derive(Debug, Clone, Serialize, Deserialize)]
1532pub struct PermissionDeniedMessage {
1533 pub tool_name: String,
1534 pub tool_use_id: String,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub agent_id: Option<String>,
1537 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub decision_reason_type: Option<String>,
1539 #[serde(default, skip_serializing_if = "Option::is_none")]
1540 pub decision_reason: Option<String>,
1541 pub message: String,
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub uuid: Option<String>,
1544 #[serde(default, skip_serializing_if = "Option::is_none")]
1545 pub session_id: Option<String>,
1546}
1547
1548#[derive(Debug, Clone, Serialize, Deserialize)]
1549pub struct MirrorErrorMessage {
1550 pub error: String,
1551 pub key: MirrorErrorKey,
1552 #[serde(default, skip_serializing_if = "Option::is_none")]
1553 pub uuid: Option<String>,
1554 #[serde(default, skip_serializing_if = "Option::is_none")]
1555 pub session_id: Option<String>,
1556}
1557
1558#[derive(Debug, Clone, Serialize, Deserialize)]
1559pub struct MirrorErrorKey {
1560 #[serde(rename = "projectKey")]
1561 pub project_key: String,
1562 #[serde(rename = "sessionId")]
1563 pub session_id: String,
1564 #[serde(default, skip_serializing_if = "Option::is_none")]
1565 pub subpath: Option<String>,
1566}
1567
1568#[derive(Debug, Clone, Serialize, Deserialize)]
1569pub struct InformationalMessage {
1570 pub content: String,
1571 pub level: String,
1572 #[serde(default, skip_serializing_if = "Option::is_none")]
1573 pub tool_use_id: Option<String>,
1574 #[serde(default, skip_serializing_if = "Option::is_none")]
1575 pub prevent_continuation: Option<bool>,
1576 #[serde(default, skip_serializing_if = "Option::is_none")]
1577 pub uuid: Option<String>,
1578 #[serde(default, skip_serializing_if = "Option::is_none")]
1579 pub session_id: Option<String>,
1580}
1581
1582#[derive(Debug, Clone, Serialize, Deserialize)]
1584pub struct PluginInfo {
1585 pub name: String,
1587 pub path: String,
1589 #[serde(skip_serializing_if = "Option::is_none")]
1591 pub source: Option<String>,
1592 #[serde(default, skip_serializing_if = "Option::is_none")]
1594 pub version: Option<String>,
1595}
1596
1597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1599pub struct PluginDiagnostic {
1600 pub plugin: String,
1601 #[serde(rename = "type")]
1602 pub diagnostic_type: String,
1603 pub message: String,
1604}
1605
1606#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1608pub struct MemoryPaths {
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1610 pub auto: Option<String>,
1611 #[serde(default, skip_serializing_if = "Option::is_none")]
1612 pub team: Option<String>,
1613 #[serde(flatten)]
1614 pub extra: serde_json::Map<String, Value>,
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1621pub struct McpServerError {
1622 pub name: String,
1623 #[serde(rename = "type")]
1625 pub error_type: String,
1626 pub message: String,
1627}
1628
1629#[derive(Debug, Clone, Serialize, Deserialize)]
1631pub struct InitMessage {
1632 pub session_id: String,
1634 #[serde(skip_serializing_if = "Option::is_none")]
1636 pub cwd: Option<String>,
1637 #[serde(skip_serializing_if = "Option::is_none")]
1639 pub model: Option<String>,
1640 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1642 pub tools: Vec<String>,
1643 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1645 pub mcp_servers: Vec<Value>,
1646 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1648 pub slash_commands: Vec<String>,
1649 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1652 pub terminal_slash_commands: Vec<String>,
1653 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1655 pub agents: Vec<String>,
1656 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1658 pub plugins: Vec<PluginInfo>,
1659 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1661 pub skills: Vec<Value>,
1662 #[serde(skip_serializing_if = "Option::is_none")]
1664 pub claude_code_version: Option<String>,
1665 #[serde(skip_serializing_if = "Option::is_none")]
1668 pub messaging_socket_path: Option<String>,
1669 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1671 pub api_key_source: Option<ApiKeySource>,
1672 #[serde(skip_serializing_if = "Option::is_none")]
1674 pub output_style: Option<OutputStyle>,
1675 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1677 pub permission_mode: Option<InitPermissionMode>,
1678
1679 #[serde(skip_serializing_if = "Option::is_none")]
1681 pub uuid: Option<String>,
1682
1683 #[serde(skip_serializing_if = "Option::is_none")]
1685 pub memory_paths: Option<MemoryPaths>,
1686
1687 #[serde(skip_serializing_if = "Option::is_none")]
1689 pub fast_mode_state: Option<String>,
1690
1691 #[serde(default, skip_serializing_if = "Option::is_none")]
1693 pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
1694
1695 #[serde(default, skip_serializing_if = "Option::is_none")]
1698 pub mcp_server_errors: Option<Vec<McpServerError>>,
1699
1700 #[serde(default, skip_serializing_if = "Option::is_none")]
1702 pub analytics_disabled: Option<bool>,
1703
1704 #[serde(default, skip_serializing_if = "Option::is_none")]
1706 pub product_feedback_disabled: Option<bool>,
1707
1708 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1710 pub betas: Vec<String>,
1711
1712 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1714 pub capabilities: Vec<String>,
1715
1716 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1718 pub plugin_errors: Vec<PluginDiagnostic>,
1719
1720 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1722 pub plugin_warnings: Vec<PluginDiagnostic>,
1723
1724 #[serde(default, skip_serializing_if = "Option::is_none")]
1729 pub effort: Option<String>,
1730
1731 #[serde(default, skip_serializing_if = "Option::is_none")]
1736 pub cloud_session: Option<Value>,
1737}
1738
1739#[derive(Debug, Clone, Serialize, Deserialize)]
1741pub struct StatusMessage {
1742 pub session_id: String,
1744 pub status: Option<StatusMessageStatus>,
1746 #[serde(skip_serializing_if = "Option::is_none")]
1748 pub uuid: Option<String>,
1749 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1751 pub permission_mode: Option<InitPermissionMode>,
1752 #[serde(skip_serializing_if = "Option::is_none")]
1753 pub compact_result: Option<String>,
1754 #[serde(skip_serializing_if = "Option::is_none")]
1755 pub compact_error: Option<String>,
1756}
1757
1758#[derive(Debug, Clone, Serialize, Deserialize)]
1760pub struct CompactBoundaryMessage {
1761 pub session_id: String,
1763 pub compact_metadata: CompactMetadata,
1765 #[serde(
1769 default,
1770 skip_serializing_if = "Option::is_none",
1771 alias = "content",
1772 alias = "text"
1773 )]
1774 pub summary: Option<String>,
1775 #[serde(
1779 default,
1780 skip_serializing_if = "Option::is_none",
1781 alias = "message_count"
1782 )]
1783 pub leaf_message_count: Option<u32>,
1784 #[serde(default, skip_serializing_if = "Option::is_none")]
1786 pub duration_ms: Option<u64>,
1787 #[serde(skip_serializing_if = "Option::is_none")]
1789 pub uuid: Option<String>,
1790 #[serde(skip_serializing_if = "Option::is_none")]
1792 pub logical_parent_uuid: Option<Option<String>>,
1793}
1794
1795#[derive(Debug, Clone, Serialize, Deserialize)]
1797pub struct CompactMetadata {
1798 pub pre_tokens: u64,
1800 pub trigger: CompactionTrigger,
1802 #[serde(default, skip_serializing_if = "Option::is_none")]
1803 pub post_tokens: Option<u64>,
1804 #[serde(default, skip_serializing_if = "Option::is_none")]
1805 pub cumulative_dropped_tokens: Option<u64>,
1806 #[serde(default, skip_serializing_if = "Option::is_none")]
1807 pub duration_ms: Option<u64>,
1808 #[serde(default, skip_serializing_if = "Option::is_none")]
1809 pub user_context: Option<String>,
1810 #[serde(default, skip_serializing_if = "Option::is_none")]
1811 pub messages_summarized: Option<u64>,
1812 #[serde(default, skip_serializing_if = "Option::is_none")]
1813 pub precomputed: Option<bool>,
1814 #[serde(default, skip_serializing_if = "Option::is_none")]
1815 pub pre_compact_discovered_tools: Option<Vec<String>>,
1816 #[serde(default, skip_serializing_if = "Option::is_none")]
1817 pub preserved_segment: Option<PreservedSegment>,
1818 #[serde(default, skip_serializing_if = "Option::is_none")]
1819 pub preserved_messages: Option<PreservedMessages>,
1820}
1821
1822#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1823pub struct PreservedSegment {
1824 pub head_uuid: String,
1825 pub anchor_uuid: String,
1826 pub tail_uuid: String,
1827}
1828
1829#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1830pub struct PreservedMessages {
1831 pub anchor_uuid: String,
1832 pub uuids: Vec<String>,
1833 #[serde(default, skip_serializing_if = "Option::is_none")]
1834 pub all_uuids: Option<Vec<String>>,
1835}
1836
1837#[derive(Debug, Clone, Serialize, Deserialize)]
1843pub struct TaskUsage {
1844 pub duration_ms: u64,
1846 pub tool_uses: u64,
1848 pub total_tokens: u64,
1850}
1851
1852#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1854pub enum TaskType {
1855 LocalAgent,
1857 LocalBash,
1859 LocalWorkflow,
1861 Unknown(String),
1863}
1864
1865impl TaskType {
1866 pub fn as_str(&self) -> &str {
1867 match self {
1868 Self::LocalAgent => "local_agent",
1869 Self::LocalBash => "local_bash",
1870 Self::LocalWorkflow => "local_workflow",
1871 Self::Unknown(s) => s.as_str(),
1872 }
1873 }
1874}
1875
1876impl fmt::Display for TaskType {
1877 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1878 f.write_str(self.as_str())
1879 }
1880}
1881
1882impl From<&str> for TaskType {
1883 fn from(s: &str) -> Self {
1884 match s {
1885 "local_agent" => Self::LocalAgent,
1886 "local_bash" => Self::LocalBash,
1887 "local_workflow" => Self::LocalWorkflow,
1888 other => Self::Unknown(other.to_string()),
1889 }
1890 }
1891}
1892
1893impl Serialize for TaskType {
1894 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1895 serializer.serialize_str(self.as_str())
1896 }
1897}
1898
1899impl<'de> Deserialize<'de> for TaskType {
1900 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1901 let s = String::deserialize(deserializer)?;
1902 Ok(Self::from(s.as_str()))
1903 }
1904}
1905
1906#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1908pub enum TaskStatus {
1909 Pending,
1910 Running,
1911 Completed,
1912 Failed,
1913 Killed,
1914 Paused,
1915 Stopped,
1916 Unknown(String),
1917}
1918
1919impl TaskStatus {
1920 pub fn as_str(&self) -> &str {
1921 match self {
1922 Self::Pending => "pending",
1923 Self::Running => "running",
1924 Self::Completed => "completed",
1925 Self::Failed => "failed",
1926 Self::Killed => "killed",
1927 Self::Paused => "paused",
1928 Self::Stopped => "stopped",
1929 Self::Unknown(s) => s.as_str(),
1930 }
1931 }
1932}
1933
1934impl fmt::Display for TaskStatus {
1935 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1936 f.write_str(self.as_str())
1937 }
1938}
1939
1940impl From<&str> for TaskStatus {
1941 fn from(s: &str) -> Self {
1942 match s {
1943 "pending" => Self::Pending,
1944 "running" => Self::Running,
1945 "completed" => Self::Completed,
1946 "failed" => Self::Failed,
1947 "killed" => Self::Killed,
1948 "paused" => Self::Paused,
1949 "stopped" => Self::Stopped,
1950 other => Self::Unknown(other.to_string()),
1951 }
1952 }
1953}
1954
1955impl Serialize for TaskStatus {
1956 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1957 serializer.serialize_str(self.as_str())
1958 }
1959}
1960
1961impl<'de> Deserialize<'de> for TaskStatus {
1962 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1963 let s = String::deserialize(deserializer)?;
1964 Ok(Self::from(s.as_str()))
1965 }
1966}
1967
1968#[derive(Debug, Clone, Serialize, Deserialize)]
1970pub struct TaskStartedMessage {
1971 pub session_id: String,
1972 pub task_id: String,
1973 #[serde(default, skip_serializing_if = "Option::is_none")]
1974 pub task_type: Option<TaskType>,
1975 #[serde(default, skip_serializing_if = "Option::is_none")]
1976 pub tool_use_id: Option<String>,
1977 pub description: String,
1978 #[serde(default, skip_serializing_if = "Option::is_none")]
1981 pub subagent_type: Option<String>,
1982 #[serde(default, skip_serializing_if = "Option::is_none")]
1988 pub is_backgrounded: Option<bool>,
1989 #[serde(default, skip_serializing_if = "Option::is_none")]
1993 pub spawn_depth: Option<u32>,
1994 #[serde(default, skip_serializing_if = "Option::is_none")]
1996 pub prompt: Option<String>,
1997 #[serde(default, skip_serializing_if = "Option::is_none")]
1998 pub workflow_name: Option<String>,
1999 #[serde(default, skip_serializing_if = "Option::is_none")]
2000 pub skip_transcript: Option<bool>,
2001 pub uuid: String,
2002}
2003
2004#[derive(Debug, Clone, Serialize, Deserialize)]
2008pub struct TaskUpdatedMessage {
2009 pub session_id: String,
2010 pub task_id: String,
2011 pub patch: TaskPatch,
2012 pub uuid: String,
2013}
2014
2015#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2018pub struct TaskPatch {
2019 #[serde(default, skip_serializing_if = "Option::is_none")]
2020 pub status: Option<TaskStatus>,
2021 #[serde(default, skip_serializing_if = "Option::is_none")]
2024 pub end_time: Option<u64>,
2025 #[serde(default, skip_serializing_if = "Option::is_none")]
2026 pub description: Option<String>,
2027 #[serde(default, skip_serializing_if = "Option::is_none")]
2028 pub total_paused_ms: Option<u64>,
2029 #[serde(default, skip_serializing_if = "Option::is_none")]
2030 pub error: Option<String>,
2031 #[serde(default, skip_serializing_if = "Option::is_none")]
2032 pub is_backgrounded: Option<bool>,
2033}
2034
2035#[derive(Debug, Clone, Serialize, Deserialize)]
2038pub struct ThinkingTokensMessage {
2039 pub session_id: String,
2040 pub estimated_tokens: u64,
2042 pub estimated_tokens_delta: u64,
2044 pub uuid: String,
2045}
2046
2047#[derive(Debug, Clone, Serialize, Deserialize)]
2050pub struct TaskProgressMessage {
2051 pub session_id: String,
2052 pub task_id: String,
2053 #[serde(default, skip_serializing_if = "Option::is_none")]
2054 pub tool_use_id: Option<String>,
2055 pub description: String,
2056 #[serde(default, skip_serializing_if = "Option::is_none")]
2057 pub last_tool_name: Option<String>,
2058 pub usage: TaskUsage,
2059 #[serde(default, skip_serializing_if = "Option::is_none")]
2061 pub subagent_type: Option<String>,
2062 #[serde(default, skip_serializing_if = "Option::is_none")]
2063 pub summary: Option<String>,
2064 pub uuid: String,
2065}
2066
2067#[derive(Debug, Clone, Serialize, Deserialize)]
2070pub struct TaskNotificationMessage {
2071 pub session_id: String,
2072 pub task_id: String,
2073 pub status: TaskStatus,
2074 pub summary: String,
2075 pub output_file: Option<String>,
2076 #[serde(skip_serializing_if = "Option::is_none")]
2077 pub tool_use_id: Option<String>,
2078 #[serde(skip_serializing_if = "Option::is_none")]
2079 pub usage: Option<TaskUsage>,
2080 #[serde(default, skip_serializing_if = "Option::is_none")]
2081 pub skip_transcript: Option<bool>,
2082 #[serde(skip_serializing_if = "Option::is_none")]
2083 pub uuid: Option<String>,
2084}
2085
2086#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2088pub enum AssistantErrorKind {
2089 AuthenticationFailed,
2090 OauthOrgNotAllowed,
2091 BillingError,
2092 RateLimit,
2093 Overloaded,
2094 InvalidRequest,
2095 ModelNotFound,
2096 ServerError,
2097 UnknownError,
2098 MaxOutputTokens,
2099 Unknown(String),
2100}
2101
2102impl AssistantErrorKind {
2103 pub fn as_str(&self) -> &str {
2104 match self {
2105 Self::AuthenticationFailed => "authentication_failed",
2106 Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
2107 Self::BillingError => "billing_error",
2108 Self::RateLimit => "rate_limit",
2109 Self::Overloaded => "overloaded",
2110 Self::InvalidRequest => "invalid_request",
2111 Self::ModelNotFound => "model_not_found",
2112 Self::ServerError => "server_error",
2113 Self::UnknownError => "unknown",
2114 Self::MaxOutputTokens => "max_output_tokens",
2115 Self::Unknown(s) => s.as_str(),
2116 }
2117 }
2118}
2119
2120impl fmt::Display for AssistantErrorKind {
2121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2122 f.write_str(self.as_str())
2123 }
2124}
2125
2126impl From<&str> for AssistantErrorKind {
2127 fn from(s: &str) -> Self {
2128 match s {
2129 "authentication_failed" => Self::AuthenticationFailed,
2130 "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
2131 "billing_error" => Self::BillingError,
2132 "rate_limit" => Self::RateLimit,
2133 "overloaded" => Self::Overloaded,
2134 "invalid_request" => Self::InvalidRequest,
2135 "model_not_found" => Self::ModelNotFound,
2136 "server_error" => Self::ServerError,
2137 "unknown" => Self::UnknownError,
2138 "max_output_tokens" => Self::MaxOutputTokens,
2139 other => Self::Unknown(other.to_string()),
2140 }
2141 }
2142}
2143
2144impl Serialize for AssistantErrorKind {
2145 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2146 serializer.serialize_str(self.as_str())
2147 }
2148}
2149
2150impl<'de> Deserialize<'de> for AssistantErrorKind {
2151 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2152 let s = String::deserialize(deserializer)?;
2153 Ok(Self::from(s.as_str()))
2154 }
2155}
2156
2157#[derive(Debug, Clone, Serialize, Deserialize)]
2164pub struct CodeChangePublishedMessage {
2165 pub provider: String,
2169 pub url: String,
2171 pub repo: String,
2174 pub identifier: String,
2176 #[serde(default, skip_serializing_if = "Option::is_none")]
2184 pub action: Option<String>,
2185 pub uuid: String,
2186 pub session_id: String,
2187}
2188
2189#[derive(Debug, Clone, Serialize, Deserialize)]
2194pub struct VcsStateChangedMessage {
2195 pub kind: VcsMutationKind,
2198 pub cwd: String,
2201 #[serde(default, skip_serializing_if = "Option::is_none")]
2206 pub branch: Option<String>,
2207 pub uuid: String,
2208 pub session_id: String,
2209}
2210
2211#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2213pub enum VcsMutationKind {
2214 Commit,
2215 Push,
2216 Merge,
2217 Rebase,
2218 Unknown(String),
2220}
2221
2222impl VcsMutationKind {
2223 pub fn as_str(&self) -> &str {
2224 match self {
2225 Self::Commit => "commit",
2226 Self::Push => "push",
2227 Self::Merge => "merge",
2228 Self::Rebase => "rebase",
2229 Self::Unknown(s) => s.as_str(),
2230 }
2231 }
2232}
2233
2234impl fmt::Display for VcsMutationKind {
2235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2236 f.write_str(self.as_str())
2237 }
2238}
2239
2240impl From<&str> for VcsMutationKind {
2241 fn from(s: &str) -> Self {
2242 match s {
2243 "commit" => Self::Commit,
2244 "push" => Self::Push,
2245 "merge" => Self::Merge,
2246 "rebase" => Self::Rebase,
2247 other => Self::Unknown(other.to_string()),
2248 }
2249 }
2250}
2251
2252impl Serialize for VcsMutationKind {
2253 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2254 serializer.serialize_str(self.as_str())
2255 }
2256}
2257
2258impl<'de> Deserialize<'de> for VcsMutationKind {
2259 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2260 let s = String::deserialize(deserializer)?;
2261 Ok(Self::from(s.as_str()))
2262 }
2263}
2264
2265#[derive(Debug, Clone, Serialize, Deserialize)]
2267pub struct FeedbackDraftQueuedMessage {
2268 pub draft_id: String,
2269 pub draft_type: String,
2270 pub title: String,
2271 pub details_preview: String,
2272 #[serde(default, skip_serializing_if = "Option::is_none")]
2273 pub uuid: Option<String>,
2274 #[serde(default, skip_serializing_if = "Option::is_none")]
2275 pub session_id: Option<String>,
2276 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2277 pub extra: serde_json::Map<String, Value>,
2278}
2279
2280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2283pub struct BatchToolUse {
2284 pub id: String,
2285 pub name: String,
2286}
2287
2288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2293pub struct ContextUsage {
2294 pub model: String,
2296 pub total_tokens: u64,
2299 pub raw_max_tokens: u64,
2302 pub percentage: u64,
2304 #[serde(default, skip_serializing_if = "Option::is_none")]
2306 pub over_limit: Option<ContextOverLimit>,
2307 #[serde(default)]
2309 pub categories: Vec<ContextCategory>,
2310 #[serde(default)]
2312 pub mcp_tools: Vec<ContextMcpTool>,
2313 #[serde(default)]
2315 pub memory_files: Vec<ContextMemoryFile>,
2316 #[serde(default)]
2318 pub agents: Vec<ContextAgent>,
2319 #[serde(default, skip_serializing_if = "Option::is_none")]
2321 pub skills: Option<Vec<ContextSkill>>,
2322}
2323
2324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2326pub struct ContextOverLimit {
2327 pub tokens_over: u64,
2328 pub kind: String,
2331}
2332
2333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2335pub struct ContextCategory {
2336 pub name: String,
2339 pub tokens: u64,
2340 pub kind: String,
2344}
2345
2346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2348pub struct ContextMcpTool {
2349 pub name: String,
2351 pub server_name: String,
2352 pub tokens: u64,
2353}
2354
2355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2357pub struct ContextMemoryFile {
2358 pub path: String,
2359 #[serde(rename = "type")]
2361 pub file_type: String,
2362 pub tokens: u64,
2363}
2364
2365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2367pub struct ContextAgent {
2368 pub agent_type: String,
2369 pub source: String,
2371 pub tokens: u64,
2372}
2373
2374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2376pub struct ContextSkill {
2377 pub name: String,
2378 pub source: String,
2380 #[serde(default, skip_serializing_if = "Option::is_none")]
2381 pub plugin_name: Option<String>,
2382 pub tokens: u64,
2383}
2384
2385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2387pub struct ToolUseMeta {
2388 pub id: String,
2389 pub display_name: String,
2390 #[serde(default, skip_serializing_if = "Option::is_none")]
2391 pub server_display_name: Option<String>,
2392 #[serde(default, skip_serializing_if = "Option::is_none")]
2393 pub icon_url: Option<String>,
2394}
2395
2396#[derive(Debug, Clone, Serialize, Deserialize)]
2398pub struct AssistantMessage {
2399 pub message: AssistantMessageContent,
2400 #[serde(alias = "sessionId")]
2401 pub session_id: String,
2402 #[serde(skip_serializing_if = "Option::is_none")]
2403 pub uuid: Option<String>,
2404 #[serde(skip_serializing_if = "Option::is_none")]
2405 pub parent_tool_use_id: Option<String>,
2406 #[serde(skip_serializing_if = "Option::is_none")]
2408 pub request_id: Option<String>,
2409 #[serde(skip_serializing_if = "Option::is_none")]
2412 pub subagent_type: Option<String>,
2413 #[serde(skip_serializing_if = "Option::is_none")]
2415 pub task_description: Option<String>,
2416 #[serde(skip_serializing_if = "Option::is_none")]
2417 pub error: Option<AssistantErrorKind>,
2418 #[serde(default, skip_serializing_if = "Option::is_none")]
2422 pub aborted: Option<bool>,
2423 #[serde(default, skip_serializing_if = "Option::is_none")]
2428 pub resumed_from_incomplete_thinking: Option<bool>,
2429 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2430 pub supersedes: Vec<String>,
2431 #[serde(skip_serializing_if = "Option::is_none")]
2432 pub timestamp: Option<String>,
2433 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2434 pub tool_use_meta: Vec<ToolUseMeta>,
2435 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2440 pub batch_tool_uses: Vec<BatchToolUse>,
2441 #[serde(default, skip_serializing_if = "Option::is_none")]
2445 pub context_usage: Option<ContextUsage>,
2446 #[serde(default, skip_serializing_if = "Option::is_none")]
2447 pub is_meta: Option<bool>,
2448 #[serde(default, skip_serializing_if = "Option::is_none")]
2449 pub is_virtual: Option<bool>,
2450 #[serde(default, skip_serializing_if = "Option::is_none")]
2451 pub is_api_error_message: Option<bool>,
2452 #[serde(skip_serializing_if = "Option::is_none")]
2453 pub api_error_status: Option<u16>,
2454 #[serde(skip_serializing_if = "Option::is_none")]
2455 pub api_error: Option<String>,
2456 #[serde(skip_serializing_if = "Option::is_none")]
2457 pub error_details: Option<String>,
2458 #[serde(skip_serializing_if = "Option::is_none")]
2459 pub advisor_model: Option<String>,
2460 #[serde(skip_serializing_if = "Option::is_none")]
2461 pub attribution_agent: Option<String>,
2462 #[serde(skip_serializing_if = "Option::is_none")]
2463 pub attribution_skill: Option<String>,
2464 #[serde(skip_serializing_if = "Option::is_none")]
2465 pub attribution_plugin: Option<String>,
2466 #[serde(skip_serializing_if = "Option::is_none")]
2467 pub attribution_mcp_server: Option<String>,
2468 #[serde(skip_serializing_if = "Option::is_none")]
2469 pub attribution_mcp_tool: Option<String>,
2470}
2471
2472#[derive(Debug, Clone, Serialize, Deserialize)]
2474pub struct AssistantMessageContent {
2475 pub id: String,
2476 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2478 pub message_type: Option<String>,
2479 pub role: MessageRole,
2480 pub model: String,
2481 pub content: Vec<ContentBlock>,
2482 #[serde(skip_serializing_if = "Option::is_none")]
2483 pub stop_reason: Option<StopReason>,
2484 #[serde(skip_serializing_if = "Option::is_none")]
2485 pub stop_sequence: Option<String>,
2486 #[serde(skip_serializing_if = "Option::is_none")]
2487 pub usage: Option<AssistantUsage>,
2488 #[serde(skip_serializing_if = "Option::is_none")]
2490 pub stop_details: Option<Value>,
2491 #[serde(skip_serializing_if = "Option::is_none")]
2493 pub context_management: Option<Value>,
2494}
2495
2496#[derive(Debug, Clone, Serialize, Deserialize)]
2498pub struct AssistantUsage {
2499 #[serde(default)]
2501 pub input_tokens: u32,
2502
2503 #[serde(default)]
2505 pub output_tokens: u32,
2506
2507 #[serde(default)]
2509 pub cache_creation_input_tokens: u32,
2510
2511 #[serde(default)]
2513 pub cache_read_input_tokens: u32,
2514
2515 #[serde(skip_serializing_if = "Option::is_none")]
2517 pub service_tier: Option<String>,
2518
2519 #[serde(skip_serializing_if = "Option::is_none")]
2521 pub cache_creation: Option<CacheCreationDetails>,
2522
2523 #[serde(skip_serializing_if = "Option::is_none")]
2525 pub inference_geo: Option<String>,
2526}
2527
2528#[derive(Debug, Clone, Serialize, Deserialize)]
2530pub struct CacheCreationDetails {
2531 #[serde(default)]
2533 pub ephemeral_1h_input_tokens: u32,
2534
2535 #[serde(default)]
2537 pub ephemeral_5m_input_tokens: u32,
2538}
2539
2540#[cfg(test)]
2541mod tests {
2542 use crate::io::ClaudeOutput;
2543
2544 #[test]
2545 fn test_subagent_usage_rollup_accumulates_task_results() {
2546 use super::SubagentUsageRollup;
2547
2548 let mut rollup = SubagentUsageRollup::default();
2549
2550 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}}"#;
2551 let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2552 assert!(rollup.observe(&output));
2553 assert_eq!(rollup.subagent_tokens, 10201);
2554 assert_eq!(rollup.agent_count, 1);
2555 assert_eq!(rollup.tool_uses, 3);
2556 assert_eq!(rollup.duration_ms, 1853);
2557
2558 assert!(!rollup.observe(&output));
2560 assert_eq!(rollup.agent_count, 1);
2561 assert_eq!(rollup.subagent_tokens, 10201);
2562
2563 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}}"#;
2565 let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2566 assert!(rollup.observe(&output));
2567 assert_eq!(rollup.agent_count, 2);
2568 assert_eq!(rollup.subagent_tokens, 10701);
2569 }
2570
2571 #[test]
2572 fn test_subagent_usage_rollup_ignores_non_task_results() {
2573 use super::SubagentUsageRollup;
2574
2575 let mut rollup = SubagentUsageRollup::default();
2576
2577 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}}"#;
2580 let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2581 assert!(!rollup.observe(&output));
2582
2583 let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2585 let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2586 assert!(!rollup.observe(&output));
2587
2588 let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2590 let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2591 assert!(!rollup.observe(&output));
2592
2593 assert_eq!(rollup, SubagentUsageRollup::default());
2594 }
2595
2596 #[test]
2597 fn test_subagent_usage_rollup_over_captured_session() {
2598 use super::SubagentUsageRollup;
2599
2600 let mut rollup = SubagentUsageRollup::default();
2601 let fixture =
2602 include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2603 for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2604 if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2605 rollup.observe(&output);
2606 }
2607 }
2608 assert_eq!(rollup.agent_count, 1);
2609 assert_eq!(rollup.subagent_tokens, 10201);
2610 }
2611
2612 #[test]
2613 fn test_system_message_init() {
2614 let json = r#"{
2615 "type": "system",
2616 "subtype": "init",
2617 "session_id": "test-session-123",
2618 "cwd": "/home/user/project",
2619 "model": "claude-sonnet-4",
2620 "tools": ["Bash", "Read", "Write"],
2621 "mcp_servers": [],
2622 "slash_commands": ["compact", "cost", "review"],
2623 "agents": ["Bash", "Explore", "Plan"],
2624 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2625 "skills": [],
2626 "claude_code_version": "2.1.15",
2627 "apiKeySource": "none",
2628 "output_style": "default",
2629 "permissionMode": "default"
2630 }"#;
2631
2632 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2633 if let ClaudeOutput::System(sys) = output {
2634 assert!(sys.is_init());
2635 assert!(!sys.is_status());
2636 assert!(!sys.is_compact_boundary());
2637
2638 let init = sys.as_init().expect("Should parse as init");
2639 assert_eq!(init.session_id, "test-session-123");
2640 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2641 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2642 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2643 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2644 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2645 assert_eq!(init.plugins.len(), 1);
2646 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2647 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2648 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2649 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2650 assert_eq!(
2651 init.permission_mode,
2652 Some(super::InitPermissionMode::Default)
2653 );
2654 } else {
2655 panic!("Expected System message");
2656 }
2657 }
2658
2659 #[test]
2660 fn test_system_message_init_from_real_capture() {
2661 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2662 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2663 if let ClaudeOutput::System(sys) = output {
2664 let init = sys.as_init().expect("Should parse real init capture");
2665 assert_eq!(init.slash_commands.len(), 8);
2666 assert!(init.slash_commands.contains(&"compact".to_string()));
2667 assert!(init.slash_commands.contains(&"review".to_string()));
2668 assert_eq!(init.agents.len(), 5);
2669 assert!(init.agents.contains(&"Bash".to_string()));
2670 assert!(init.agents.contains(&"Explore".to_string()));
2671 assert_eq!(init.plugins.len(), 1);
2672 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2673 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2674 } else {
2675 panic!("Expected System message");
2676 }
2677 }
2678
2679 #[test]
2680 fn test_system_message_status() {
2681 let json = r#"{
2682 "type": "system",
2683 "subtype": "status",
2684 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2685 "status": "compacting",
2686 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2687 }"#;
2688
2689 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2690 if let ClaudeOutput::System(sys) = output {
2691 assert!(sys.is_status());
2692 assert!(!sys.is_init());
2693
2694 let status = sys.as_status().expect("Should parse as status");
2695 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2696 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
2697 assert_eq!(
2698 status.uuid,
2699 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2700 );
2701 } else {
2702 panic!("Expected System message");
2703 }
2704 }
2705
2706 #[test]
2707 fn test_system_message_status_null() {
2708 let json = r#"{
2709 "type": "system",
2710 "subtype": "status",
2711 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2712 "status": null,
2713 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2714 }"#;
2715
2716 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2717 if let ClaudeOutput::System(sys) = output {
2718 let status = sys.as_status().expect("Should parse as status");
2719 assert_eq!(status.status, None);
2720 } else {
2721 panic!("Expected System message");
2722 }
2723 }
2724
2725 #[test]
2726 fn test_system_message_task_started() {
2727 let json = r#"{
2728 "type": "system",
2729 "subtype": "task_started",
2730 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2731 "task_id": "b6daf3f",
2732 "task_type": "local_bash",
2733 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
2734 "description": "Wait for CI on PR #12",
2735 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
2736 }"#;
2737
2738 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2739 if let ClaudeOutput::System(sys) = output {
2740 assert!(sys.is_task_started());
2741 assert!(!sys.is_task_progress());
2742 assert!(!sys.is_task_notification());
2743
2744 let task = sys.as_task_started().expect("Should parse as task_started");
2745 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
2746 assert_eq!(task.task_id, "b6daf3f");
2747 assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
2748 assert_eq!(
2749 task.tool_use_id.as_deref(),
2750 Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
2751 );
2752 assert_eq!(task.description, "Wait for CI on PR #12");
2753 } else {
2754 panic!("Expected System message");
2755 }
2756 }
2757
2758 #[test]
2759 fn test_system_message_task_started_agent() {
2760 let json = r#"{
2761 "type": "system",
2762 "subtype": "task_started",
2763 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2764 "task_id": "a4a7e0906e5fc64cc",
2765 "task_type": "local_agent",
2766 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2767 "description": "Explore Scene/ArrayScene duplication",
2768 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2769 }"#;
2770
2771 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2772 if let ClaudeOutput::System(sys) = output {
2773 let task = sys.as_task_started().expect("Should parse as task_started");
2774 assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
2775 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
2776 } else {
2777 panic!("Expected System message");
2778 }
2779 }
2780
2781 #[test]
2782 fn test_system_message_task_progress() {
2783 let json = r#"{
2784 "type": "system",
2785 "subtype": "task_progress",
2786 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2787 "task_id": "a4a7e0906e5fc64cc",
2788 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2789 "description": "Reading src/jplephem/chebyshev.rs",
2790 "last_tool_name": "Read",
2791 "usage": {
2792 "duration_ms": 13996,
2793 "tool_uses": 9,
2794 "total_tokens": 38779
2795 },
2796 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2797 }"#;
2798
2799 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2800 if let ClaudeOutput::System(sys) = output {
2801 assert!(sys.is_task_progress());
2802 assert!(!sys.is_task_started());
2803
2804 let progress = sys
2805 .as_task_progress()
2806 .expect("Should parse as task_progress");
2807 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
2808 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
2809 assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
2810 assert_eq!(progress.usage.duration_ms, 13996);
2811 assert_eq!(progress.usage.tool_uses, 9);
2812 assert_eq!(progress.usage.total_tokens, 38779);
2813 } else {
2814 panic!("Expected System message");
2815 }
2816 }
2817
2818 #[test]
2819 fn test_system_message_task_notification_completed() {
2820 let json = r#"{
2821 "type": "system",
2822 "subtype": "task_notification",
2823 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2824 "task_id": "a0ba761e9dc9c316f",
2825 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
2826 "status": "completed",
2827 "summary": "Agent \"Write Hipparcos data source doc\" completed",
2828 "output_file": "",
2829 "usage": {
2830 "duration_ms": 172300,
2831 "tool_uses": 11,
2832 "total_tokens": 42005
2833 },
2834 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
2835 }"#;
2836
2837 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2838 if let ClaudeOutput::System(sys) = output {
2839 assert!(sys.is_task_notification());
2840
2841 let notif = sys
2842 .as_task_notification()
2843 .expect("Should parse as task_notification");
2844 assert_eq!(notif.status, super::TaskStatus::Completed);
2845 assert_eq!(
2846 notif.summary,
2847 "Agent \"Write Hipparcos data source doc\" completed"
2848 );
2849 assert_eq!(notif.output_file, Some("".to_string()));
2850 assert_eq!(
2851 notif.tool_use_id,
2852 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
2853 );
2854 let usage = notif.usage.expect("Should have usage");
2855 assert_eq!(usage.duration_ms, 172300);
2856 assert_eq!(usage.tool_uses, 11);
2857 assert_eq!(usage.total_tokens, 42005);
2858 } else {
2859 panic!("Expected System message");
2860 }
2861 }
2862
2863 #[test]
2864 fn test_system_message_task_notification_failed_no_usage() {
2865 let json = r#"{
2866 "type": "system",
2867 "subtype": "task_notification",
2868 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
2869 "task_id": "b98f6a3",
2870 "status": "failed",
2871 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
2872 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
2873 }"#;
2874
2875 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2876 if let ClaudeOutput::System(sys) = output {
2877 let notif = sys
2878 .as_task_notification()
2879 .expect("Should parse as task_notification");
2880 assert_eq!(notif.status, super::TaskStatus::Failed);
2881 assert!(notif.tool_use_id.is_none());
2882 assert!(notif.usage.is_none());
2883 assert_eq!(
2884 notif.output_file,
2885 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
2886 );
2887 } else {
2888 panic!("Expected System message");
2889 }
2890 }
2891
2892 #[test]
2898 fn test_task_messages_roundtrip_through_value() {
2899 let cases = [
2900 r#"{"type":"system","subtype":"task_started","session_id":"s1",
2901 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
2902 "description":"Sleep 3s","uuid":"u1"}"#,
2903 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
2904 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
2905 "last_tool_name":"Bash",
2906 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2907 "uuid":"u2"}"#,
2908 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
2909 "task_id":"t1","tool_use_id":"tu1","status":"completed",
2910 "summary":"done","output_file":"",
2911 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2912 "uuid":"u3"}"#,
2913 ];
2914
2915 for json in cases {
2916 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2917 let value = serde_json::to_value(&output).unwrap();
2918 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
2919
2920 let ClaudeOutput::System(sys) = reparsed else {
2921 panic!("Expected System variant after round-trip");
2922 };
2923
2924 match sys.subtype {
2925 super::SystemSubtype::TaskStarted => {
2926 assert!(
2927 sys.as_task_started().is_some(),
2928 "as_task_started failed after round-trip"
2929 );
2930 }
2931 super::SystemSubtype::TaskProgress => {
2932 assert!(
2933 sys.as_task_progress().is_some(),
2934 "as_task_progress failed after round-trip"
2935 );
2936 }
2937 super::SystemSubtype::TaskNotification => {
2938 assert!(
2939 sys.as_task_notification().is_some(),
2940 "as_task_notification failed after round-trip"
2941 );
2942 }
2943 other => panic!("unexpected subtype after round-trip: {other:?}"),
2944 }
2945 }
2946 }
2947
2948 #[test]
2949 fn test_system_message_compact_boundary() {
2950 let json = r#"{
2951 "type": "system",
2952 "subtype": "compact_boundary",
2953 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2954 "compact_metadata": {
2955 "pre_tokens": 155285,
2956 "trigger": "auto"
2957 },
2958 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
2959 }"#;
2960
2961 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2962 if let ClaudeOutput::System(sys) = output {
2963 assert!(sys.is_compact_boundary());
2964 assert!(!sys.is_init());
2965 assert!(!sys.is_status());
2966
2967 let compact = sys
2968 .as_compact_boundary()
2969 .expect("Should parse as compact_boundary");
2970 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2971 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
2972 assert_eq!(
2973 compact.compact_metadata.trigger,
2974 super::CompactionTrigger::Auto
2975 );
2976 assert!(compact.summary.is_none());
2978 assert!(compact.leaf_message_count.is_none());
2979 assert!(compact.duration_ms.is_none());
2980 } else {
2981 panic!("Expected System message");
2982 }
2983 }
2984
2985 #[test]
2986 fn test_compact_boundary_with_summary_stats() {
2987 let json = r#"{
2989 "type": "system",
2990 "subtype": "compact_boundary",
2991 "session_id": "s1",
2992 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
2993 "summary": "Summarized the earlier exploration.",
2994 "leaf_message_count": 42,
2995 "duration_ms": 1234,
2996 "uuid": "u1"
2997 }"#;
2998 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2999 let ClaudeOutput::System(sys) = output else {
3000 panic!("Expected System message");
3001 };
3002 let compact = sys.as_compact_boundary().expect("compact_boundary");
3003 assert_eq!(
3004 compact.summary.as_deref(),
3005 Some("Summarized the earlier exploration.")
3006 );
3007 assert_eq!(compact.leaf_message_count, Some(42));
3008 assert_eq!(compact.duration_ms, Some(1234));
3009
3010 let json_alt = r#"{
3013 "type": "system",
3014 "subtype": "compact_boundary",
3015 "session_id": "s2",
3016 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
3017 "content": "alt-key summary",
3018 "message_count": 7
3019 }"#;
3020 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
3021 let ClaudeOutput::System(sys) = output else {
3022 panic!("Expected System message");
3023 };
3024 let compact = sys.as_compact_boundary().expect("compact_boundary");
3025 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
3026 assert_eq!(compact.leaf_message_count, Some(7));
3027 }
3028
3029 #[test]
3030 fn test_init_message_with_new_fields() {
3031 let json = r#"{
3032 "type": "system",
3033 "subtype": "init",
3034 "session_id": "test-session",
3035 "cwd": "/home/user",
3036 "model": "claude-opus-4-7",
3037 "tools": ["Bash"],
3038 "mcp_servers": [],
3039 "permissionMode": "default",
3040 "apiKeySource": "none",
3041 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
3042 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
3043 "fast_mode_state": "off",
3044 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
3045 "claude_code_version": "2.1.117"
3046 }"#;
3047
3048 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3049 if let ClaudeOutput::System(sys) = output {
3050 let init = sys.as_init().expect("Should parse as init");
3051 assert_eq!(
3052 init.uuid.as_deref(),
3053 Some("44841a0d-182d-493a-86b5-79800d3d9665")
3054 );
3055 assert!(init.memory_paths.is_some());
3056 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
3057 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
3058 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
3059 } else {
3060 panic!("Expected System message");
3061 }
3062 }
3063
3064 #[test]
3065 fn test_assistant_message_with_new_fields() {
3066 let json = r#"{
3067 "type": "assistant",
3068 "message": {
3069 "id": "msg_1",
3070 "type": "message",
3071 "role": "assistant",
3072 "model": "claude-opus-4-7",
3073 "content": [{"type": "text", "text": "Hello"}],
3074 "stop_reason": "end_turn",
3075 "stop_details": null,
3076 "context_management": null,
3077 "usage": {
3078 "input_tokens": 100,
3079 "output_tokens": 10,
3080 "cache_creation_input_tokens": 50,
3081 "cache_read_input_tokens": 0,
3082 "service_tier": "standard",
3083 "inference_geo": "not_available"
3084 }
3085 },
3086 "session_id": "abc",
3087 "uuid": "msg-uuid-123"
3088 }"#;
3089
3090 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3091 if let ClaudeOutput::Assistant(asst) = output {
3092 assert_eq!(asst.message.stop_details, None);
3093 assert_eq!(asst.message.context_management, None);
3094 let usage = asst.message.usage.unwrap();
3095 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
3096 } else {
3097 panic!("Expected Assistant message");
3098 }
3099 }
3100
3101 #[test]
3102 fn test_user_message_with_new_fields() {
3103 let json = r#"{
3104 "type": "user",
3105 "message": {
3106 "role": "user",
3107 "content": [{"type": "text", "text": "Hello"}]
3108 },
3109 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3110 "parent_tool_use_id": "toolu_123",
3111 "uuid": "user-msg-456"
3112 }"#;
3113
3114 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3115 if let ClaudeOutput::User(user) = output {
3116 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
3117 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
3118 } else {
3119 panic!("Expected User message");
3120 }
3121 }
3122
3123 #[test]
3129 fn test_user_message_preserves_tool_use_result_and_timestamp() {
3130 let json = r#"{
3131 "type":"user",
3132 "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"}]},
3133 "parent_tool_use_id":null,
3134 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3135 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
3136 "timestamp":"2026-05-12T23:12:04.121Z",
3137 "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"}}
3138 }"#;
3139
3140 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3141 let user = match output {
3142 ClaudeOutput::User(u) => u,
3143 other => panic!("Expected User message, got {:?}", other.message_type()),
3144 };
3145
3146 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
3147 let raw = user
3148 .tool_use_result
3149 .as_ref()
3150 .expect("tool_use_result must be captured");
3151 assert_eq!(raw["answers"]["Color"], "Blue");
3152 assert_eq!(raw["questions"][0]["header"], "Color");
3153
3154 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
3158 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
3159 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
3160 assert_eq!(
3161 reser["tool_use_result"]["questions"][0]["question"],
3162 "Which color do you prefer?"
3163 );
3164
3165 let typed: crate::AskUserQuestionInput = user
3168 .tool_use_result_as::<crate::AskUserQuestionInput>()
3169 .expect("tool_use_result present")
3170 .expect("AskUserQuestionInput parses");
3171 assert_eq!(typed.questions.len(), 1);
3172 assert_eq!(typed.questions[0].header, "Color");
3173 let answers = typed.answers.expect("answers populated");
3174 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3175 }
3176
3177 #[test]
3180 fn test_user_message_without_tool_use_result_omits_field() {
3181 let json = r#"{
3182 "type":"user",
3183 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3184 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3185 }"#;
3186
3187 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3188 let user = match output {
3189 ClaudeOutput::User(u) => u,
3190 _ => panic!("Expected User message"),
3191 };
3192 assert!(user.tool_use_result.is_none());
3193 assert!(user.timestamp.is_none());
3194
3195 let reser = serde_json::to_value(&user).unwrap();
3196 assert!(reser.get("tool_use_result").is_none());
3197 assert!(reser.get("timestamp").is_none());
3198 }
3199
3200 #[test]
3204 fn test_subagent_result_exposes_token_accounting() {
3205 let json = r#"{
3206 "type":"user",
3207 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3208 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3209 "tool_use_result":{
3210 "status":"completed",
3211 "prompt":"Count the .rs files.",
3212 "agentId":"ac4f0276e9d4b6232",
3213 "agentType":"Explore",
3214 "content":[{"type":"text","text":"21"}],
3215 "resolvedModel":"claude-haiku-4-5-20251001",
3216 "totalDurationMs":6869,
3217 "totalTokens":7834,
3218 "totalToolUseCount":1,
3219 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3220 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3221 }
3222 }"#;
3223
3224 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3225 let user = match output {
3226 ClaudeOutput::User(u) => u,
3227 _ => panic!("Expected User message"),
3228 };
3229
3230 let result = user.subagent_result().expect("subagent result parses");
3231 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3232 assert_eq!(
3233 result.resolved_model.as_deref(),
3234 Some("claude-haiku-4-5-20251001")
3235 );
3236 assert_eq!(result.total_tokens, Some(7834));
3237 assert_eq!(result.total_duration_ms, Some(6869));
3238 assert_eq!(result.total_tool_use_count, Some(1));
3239
3240 let usage = result.usage.expect("nested usage present");
3241 assert_eq!(usage.input_tokens, 6);
3242 assert_eq!(usage.cache_read_input_tokens, 7699);
3243
3244 let stats = result.tool_stats.expect("toolStats present");
3245 assert_eq!(stats.bash_count, 1);
3246 }
3247
3248 #[test]
3252 fn test_subagent_result_absent_for_non_task_result() {
3253 let json = r#"{
3254 "type":"user",
3255 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3256 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3257 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3258 }"#;
3259
3260 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3261 let user = match output {
3262 ClaudeOutput::User(u) => u,
3263 _ => panic!("Expected User message"),
3264 };
3265
3266 let result = user.subagent_result().expect("lenient parse");
3267 assert_eq!(result.total_tokens, None);
3268 assert_eq!(result.agent_type, None);
3269 }
3270
3271 #[test]
3272 fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3273 use serde_json::Value;
3274
3275 let raw: Value = serde_json::from_str(
3276 r#"{
3277 "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3278 "fast_mode_state":"off",
3279 "fast_mode_disabled_reason":"not_first_party",
3280 "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3281 }"#,
3282 )
3283 .unwrap();
3284 crate::io::assert_fully_wrapped(&raw);
3285
3286 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3287 let ClaudeOutput::System(sys) = output else {
3288 panic!("expected System");
3289 };
3290 let init = sys.as_init().expect("parses as init");
3291 assert_eq!(
3292 init.fast_mode_disabled_reason,
3293 Some(crate::FastModeDisabledReason::NotFirstParty)
3294 );
3295 let errs = init.mcp_server_errors.unwrap();
3296 assert_eq!(errs.len(), 1);
3297 assert_eq!(errs[0].name, "broken");
3298 assert_eq!(errs[0].error_type, "invalid_config");
3299 }
3300
3301 #[test]
3302 fn test_code_change_published_fully_wrapped() {
3303 use super::{KnownSystemEvent, SystemSubtype};
3304 use serde_json::Value;
3305
3306 let raw: Value = serde_json::from_str(
3307 r#"{
3308 "type":"system","subtype":"code_change_published",
3309 "provider":"github","url":"https://github.com/owner/repo/pull/42",
3310 "repo":"owner/repo","identifier":"42",
3311 "uuid":"u1","session_id":"s1"
3312 }"#,
3313 )
3314 .unwrap();
3315 crate::io::assert_fully_wrapped(&raw);
3316
3317 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3318 let ClaudeOutput::System(sys) = output else {
3319 panic!("expected System");
3320 };
3321 assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3322 let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3323 panic!("expected CodeChangePublished event");
3324 };
3325 assert_eq!(msg.provider, "github");
3326 assert_eq!(msg.repo, "owner/repo");
3327 assert_eq!(msg.identifier, "42");
3328
3329 assert!(sys.is_code_change_published());
3330 assert!(!sys.is_vcs_state_changed());
3331 let direct = sys.as_code_change_published().expect("direct accessor");
3332 assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3333 assert!(sys.as_vcs_state_changed().is_none());
3334 }
3335
3336 #[test]
3337 fn test_feedback_draft_queued_fully_wrapped() {
3338 use super::{KnownSystemEvent, SystemSubtype};
3339 use serde_json::Value;
3340
3341 let raw: Value = serde_json::from_str(
3342 r#"{
3343 "type":"system","subtype":"feedback_draft_queued",
3344 "draft_id":"draft-1","draft_type":"bug_report",
3345 "title":"Tool output was truncated",
3346 "details_preview":"The last command omitted its final lines",
3347 "uuid":"u1","session_id":"s1","future_field":"preserved"
3348 }"#,
3349 )
3350 .unwrap();
3351 crate::io::assert_fully_wrapped(&raw);
3352
3353 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3354 let ClaudeOutput::System(sys) = output else {
3355 panic!("expected System");
3356 };
3357 assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3358 assert!(sys.is_feedback_draft_queued());
3359 assert!(!sys.is_vcs_state_changed());
3360
3361 let direct = sys
3362 .as_feedback_draft_queued()
3363 .expect("direct typed accessor");
3364 assert_eq!(direct.draft_id, "draft-1");
3365 assert_eq!(direct.draft_type, "bug_report");
3366 assert_eq!(direct.extra["future_field"], "preserved");
3367
3368 let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3369 panic!("expected FeedbackDraftQueued event");
3370 };
3371 assert_eq!(known.title, "Tool output was truncated");
3372 assert_eq!(
3373 sys.typed_value().expect("typed value")["future_field"],
3374 "preserved"
3375 );
3376 }
3377
3378 #[test]
3379 fn test_vcs_state_changed_fully_wrapped() {
3380 use super::{KnownSystemEvent, VcsMutationKind};
3381 use serde_json::Value;
3382
3383 for kind in ["commit", "push", "merge", "rebase"] {
3384 let raw: Value = serde_json::from_str(&format!(
3385 r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3386 kind
3387 ))
3388 .unwrap();
3389 crate::io::assert_fully_wrapped(&raw);
3390
3391 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3392 let ClaudeOutput::System(sys) = output else {
3393 panic!("expected System");
3394 };
3395 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3396 panic!("expected VcsStateChanged event");
3397 };
3398 assert_eq!(msg.kind.as_str(), kind);
3399 assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3400 }
3401
3402 let raw: Value = serde_json::from_str(
3404 r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3405 )
3406 .unwrap();
3407 crate::io::assert_fully_wrapped(&raw);
3408 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3409 let ClaudeOutput::System(sys) = output else {
3410 panic!("expected System");
3411 };
3412 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3413 panic!("expected VcsStateChanged event");
3414 };
3415 assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3416
3417 assert!(sys.is_vcs_state_changed());
3418 let direct = sys.as_vcs_state_changed().expect("direct accessor");
3419 assert_eq!(direct.cwd, "/repo");
3420 assert!(sys.as_code_change_published().is_none());
3421 }
3422
3423 #[test]
3424 fn test_assistant_aborted_and_resume_flags_roundtrip() {
3425 let json = r#"{
3426 "type":"assistant",
3427 "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3428 "session_id":"s1",
3429 "aborted":true,
3430 "resumed_from_incomplete_thinking":true
3431 }"#;
3432 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3433 let ClaudeOutput::Assistant(msg) = &output else {
3434 panic!("expected Assistant");
3435 };
3436 assert_eq!(msg.aborted, Some(true));
3437 assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3438 let reserialized = serde_json::to_string(&output).unwrap();
3439 assert!(reserialized.contains("\"aborted\":true"));
3440 assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3441
3442 let json = r#"{
3444 "type":"assistant",
3445 "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3446 "session_id":"s2"
3447 }"#;
3448 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3449 let reserialized = serde_json::to_string(&output).unwrap();
3450 assert!(!reserialized.contains("aborted"));
3451 assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3452 }
3453
3454 #[test]
3455 fn test_user_tool_result_meta_roundtrip() {
3456 let json = r#"{
3457 "type":"user",
3458 "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3459 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3460 "tool_result_meta":[
3461 {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3462 {"id":"toolu_2","non_execution_kind":"permission-rule"}
3463 ]
3464 }"#;
3465 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3466 let ClaudeOutput::User(user) = &output else {
3467 panic!("expected User");
3468 };
3469 let meta = user.tool_result_meta.as_ref().unwrap();
3470 assert_eq!(meta.len(), 2);
3471 assert_eq!(meta[0].non_execution_kind, "user-rejected");
3472 assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3473 assert_eq!(meta[1].user_feedback, None);
3474
3475 let reserialized = serde_json::to_string(&output).unwrap();
3476 assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3477 assert!(!reserialized.contains("\"user_feedback\":null"));
3478 }
3479
3480 #[test]
3484 fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3485 use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3486 let with_scope = serde_json::json!({
3487 "trigger": "refusal",
3488 "direction": "retry",
3489 "scope": "local",
3490 "original_model": "claude-fable-5",
3491 "fallback_model": "claude-opus-5",
3492 "request_id": null,
3493 "content": "Refused; retried on fallback model.",
3494 "uuid": "u1",
3495 "session_id": "s1"
3496 });
3497 let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3498 assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3499 assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3500
3501 let mut without = with_scope.clone();
3504 without.as_object_mut().unwrap().remove("scope");
3505 let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3506 assert_eq!(msg.scope, None);
3507 assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3508
3509 assert_eq!(
3511 RefusalFallbackScope::from("workspace").as_str(),
3512 "workspace"
3513 );
3514 }
3515}