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}
666
667impl UserMessage {
668 pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
676 &self,
677 ) -> Option<Result<T, serde_json::Error>> {
678 self.tool_use_result
679 .as_ref()
680 .map(|v| serde_json::from_value(v.clone()))
681 }
682
683 pub fn subagent_result(&self) -> Option<SubagentResult> {
694 self.tool_use_result
695 .as_ref()
696 .and_then(|v| serde_json::from_value(v.clone()).ok())
697 }
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct SubagentResult {
712 #[serde(skip_serializing_if = "Option::is_none")]
714 pub status: Option<String>,
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub prompt: Option<String>,
718 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
720 pub agent_id: Option<String>,
721 #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
723 pub agent_type: Option<String>,
724 #[serde(
726 default,
727 deserialize_with = "deserialize_content_blocks",
728 skip_serializing_if = "Vec::is_empty"
729 )]
730 pub content: Vec<ContentBlock>,
731 #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
733 pub resolved_model: Option<String>,
734 #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
736 pub total_duration_ms: Option<u64>,
737 #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
739 pub total_tokens: Option<u64>,
740 #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
742 pub total_tool_use_count: Option<u64>,
743 #[serde(skip_serializing_if = "Option::is_none")]
745 pub usage: Option<super::result::UsageInfo>,
746 #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
748 pub tool_stats: Option<SubagentToolStats>,
749}
750
751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
756#[serde(rename_all = "camelCase")]
757pub struct SubagentToolStats {
758 #[serde(default)]
759 pub read_count: u64,
760 #[serde(default)]
761 pub search_count: u64,
762 #[serde(default)]
763 pub bash_count: u64,
764 #[serde(default)]
765 pub edit_file_count: u64,
766 #[serde(default)]
767 pub lines_added: u64,
768 #[serde(default)]
769 pub lines_removed: u64,
770 #[serde(default)]
771 pub other_tool_count: u64,
772 #[serde(flatten)]
773 pub extra: serde_json::Map<String, Value>,
774}
775
776#[derive(Debug, Clone, Default, PartialEq, Eq)]
803pub struct SubagentUsageRollup {
804 pub subagent_tokens: u64,
807 pub agent_count: u32,
809 pub tool_uses: u64,
811 pub duration_ms: u64,
813 seen_agent_ids: std::collections::BTreeSet<String>,
814}
815
816impl SubagentUsageRollup {
817 pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
823 match output {
824 ClaudeOutput::User(user) => self.observe_user(user),
825 _ => false,
826 }
827 }
828
829 pub fn observe_user(&mut self, user: &UserMessage) -> bool {
836 let Some(result) = user.subagent_result() else {
837 return false;
838 };
839 if result.total_tokens.is_none() && result.agent_id.is_none() {
840 return false;
841 }
842 if let Some(agent_id) = &result.agent_id {
843 if !self.seen_agent_ids.insert(agent_id.clone()) {
844 return false;
845 }
846 }
847 self.agent_count += 1;
848 self.subagent_tokens += result.total_tokens.unwrap_or(0);
849 self.tool_uses += result.total_tool_use_count.unwrap_or(0);
850 self.duration_ms += result.total_duration_ms.unwrap_or(0);
851 true
852 }
853}
854
855#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct MessageContent {
858 pub role: MessageRole,
859 #[serde(deserialize_with = "deserialize_content_blocks")]
860 pub content: Vec<ContentBlock>,
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize)]
865pub struct SystemMessage {
866 pub subtype: SystemSubtype,
867 #[serde(flatten)]
868 pub data: Value, }
870
871impl SystemMessage {
872 pub fn is_init(&self) -> bool {
874 self.subtype == SystemSubtype::Init
875 }
876
877 pub fn is_status(&self) -> bool {
879 self.subtype == SystemSubtype::Status
880 }
881
882 pub fn is_compact_boundary(&self) -> bool {
884 self.subtype == SystemSubtype::CompactBoundary
885 }
886
887 pub fn as_init(&self) -> Option<InitMessage> {
889 if self.subtype != SystemSubtype::Init {
890 return None;
891 }
892 serde_json::from_value(self.data.clone()).ok()
893 }
894
895 pub fn as_status(&self) -> Option<StatusMessage> {
897 if self.subtype != SystemSubtype::Status {
898 return None;
899 }
900 serde_json::from_value(self.data.clone()).ok()
901 }
902
903 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
905 if self.subtype != SystemSubtype::CompactBoundary {
906 return None;
907 }
908 serde_json::from_value(self.data.clone()).ok()
909 }
910
911 pub fn is_task_started(&self) -> bool {
913 self.subtype == SystemSubtype::TaskStarted
914 }
915
916 pub fn is_task_progress(&self) -> bool {
918 self.subtype == SystemSubtype::TaskProgress
919 }
920
921 pub fn is_task_notification(&self) -> bool {
923 self.subtype == SystemSubtype::TaskNotification
924 }
925
926 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
928 if self.subtype != SystemSubtype::TaskStarted {
929 return None;
930 }
931 serde_json::from_value(self.data.clone()).ok()
932 }
933
934 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
936 if self.subtype != SystemSubtype::TaskProgress {
937 return None;
938 }
939 serde_json::from_value(self.data.clone()).ok()
940 }
941
942 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
944 if self.subtype != SystemSubtype::TaskNotification {
945 return None;
946 }
947 serde_json::from_value(self.data.clone()).ok()
948 }
949
950 pub fn is_task_updated(&self) -> bool {
952 self.subtype == SystemSubtype::TaskUpdated
953 }
954
955 pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
957 if self.subtype != SystemSubtype::TaskUpdated {
958 return None;
959 }
960 serde_json::from_value(self.data.clone()).ok()
961 }
962
963 pub fn is_thinking_tokens(&self) -> bool {
965 self.subtype == SystemSubtype::ThinkingTokens
966 }
967
968 pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
970 if self.subtype != SystemSubtype::ThinkingTokens {
971 return None;
972 }
973 serde_json::from_value(self.data.clone()).ok()
974 }
975
976 pub fn is_code_change_published(&self) -> bool {
978 self.subtype == SystemSubtype::CodeChangePublished
979 }
980
981 pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
983 if self.subtype != SystemSubtype::CodeChangePublished {
984 return None;
985 }
986 serde_json::from_value(self.data.clone()).ok()
987 }
988
989 pub fn is_vcs_state_changed(&self) -> bool {
991 self.subtype == SystemSubtype::VcsStateChanged
992 }
993
994 pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
996 if self.subtype != SystemSubtype::VcsStateChanged {
997 return None;
998 }
999 serde_json::from_value(self.data.clone()).ok()
1000 }
1001
1002 pub fn is_feedback_draft_queued(&self) -> bool {
1004 self.subtype == SystemSubtype::FeedbackDraftQueued
1005 }
1006
1007 pub fn as_feedback_draft_queued(&self) -> Option<FeedbackDraftQueuedMessage> {
1009 if self.subtype != SystemSubtype::FeedbackDraftQueued {
1010 return None;
1011 }
1012 serde_json::from_value(self.data.clone()).ok()
1013 }
1014
1015 pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
1017 macro_rules! parse {
1018 ($variant:ident, $ty:ty) => {
1019 serde_json::from_value::<$ty>(self.data.clone())
1020 .ok()
1021 .map(KnownSystemEvent::$variant)
1022 };
1023 }
1024
1025 match self.subtype {
1026 SystemSubtype::Init => parse!(Init, InitMessage),
1027 SystemSubtype::Status => parse!(Status, StatusMessage),
1028 SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
1029 SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
1030 SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
1031 SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
1032 SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
1033 SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
1034 SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
1035 SystemSubtype::ControlRequestProgress => {
1036 parse!(ControlRequestProgress, ControlRequestProgressMessage)
1037 }
1038 SystemSubtype::ModelRefusalFallback => {
1039 parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
1040 }
1041 SystemSubtype::ModelRefusalNoFallback => {
1042 parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
1043 }
1044 SystemSubtype::LocalCommandOutput => {
1045 parse!(LocalCommandOutput, LocalCommandOutputMessage)
1046 }
1047 SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
1048 SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
1049 SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
1050 SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
1051 SystemSubtype::BackgroundTasksChanged => {
1052 parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
1053 }
1054 SystemSubtype::SessionStateChanged => {
1055 parse!(SessionStateChanged, SessionStateChangedMessage)
1056 }
1057 SystemSubtype::WorkerShuttingDown => {
1058 parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
1059 }
1060 SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
1061 SystemSubtype::Notification => parse!(Notification, NotificationMessage),
1062 SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
1063 SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
1064 SystemSubtype::ElicitationComplete => {
1065 parse!(ElicitationComplete, ElicitationCompleteMessage)
1066 }
1067 SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1068 SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1069 SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1070 SystemSubtype::CodeChangePublished => {
1071 parse!(CodeChangePublished, CodeChangePublishedMessage)
1072 }
1073 SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
1074 SystemSubtype::FeedbackDraftQueued => {
1075 parse!(FeedbackDraftQueued, FeedbackDraftQueuedMessage)
1076 }
1077 SystemSubtype::Unknown(_) => None,
1078 }
1079 }
1080
1081 pub fn typed_value(&self) -> Option<Value> {
1090 fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1091 parsed.and_then(|v| serde_json::to_value(v).ok())
1092 }
1093 match self.subtype {
1094 SystemSubtype::Init => reserialize(self.as_init()),
1095 SystemSubtype::Status => reserialize(self.as_status()),
1096 SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1097 SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1098 SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1099 SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1100 SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1101 SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1102 SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1103 SystemSubtype::ControlRequestProgress => {
1104 reserialize(parse_system::<ControlRequestProgressMessage>(self))
1105 }
1106 SystemSubtype::ModelRefusalFallback => {
1107 reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1108 }
1109 SystemSubtype::ModelRefusalNoFallback => {
1110 reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1111 }
1112 SystemSubtype::LocalCommandOutput => {
1113 reserialize(parse_system::<LocalCommandOutputMessage>(self))
1114 }
1115 SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1116 SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1117 SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1118 SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1119 SystemSubtype::BackgroundTasksChanged => {
1120 reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1121 }
1122 SystemSubtype::SessionStateChanged => {
1123 reserialize(parse_system::<SessionStateChangedMessage>(self))
1124 }
1125 SystemSubtype::WorkerShuttingDown => {
1126 reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1127 }
1128 SystemSubtype::CommandsChanged => {
1129 reserialize(parse_system::<CommandsChangedMessage>(self))
1130 }
1131 SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1132 SystemSubtype::FilesPersisted => {
1133 reserialize(parse_system::<FilesPersistedMessage>(self))
1134 }
1135 SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1136 SystemSubtype::ElicitationComplete => {
1137 reserialize(parse_system::<ElicitationCompleteMessage>(self))
1138 }
1139 SystemSubtype::PermissionDenied => {
1140 reserialize(parse_system::<PermissionDeniedMessage>(self))
1141 }
1142 SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1143 SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1144 SystemSubtype::CodeChangePublished => {
1145 reserialize(parse_system::<CodeChangePublishedMessage>(self))
1146 }
1147 SystemSubtype::VcsStateChanged => {
1148 reserialize(parse_system::<VcsStateChangedMessage>(self))
1149 }
1150 SystemSubtype::FeedbackDraftQueued => {
1151 reserialize(parse_system::<FeedbackDraftQueuedMessage>(self))
1152 }
1153 SystemSubtype::Unknown(_) => None,
1154 }
1155 }
1156}
1157
1158fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1159 serde_json::from_value(message.data.clone()).ok()
1160}
1161
1162#[allow(clippy::large_enum_variant)]
1168#[derive(Debug, Clone, Serialize, Deserialize)]
1169pub enum KnownSystemEvent {
1170 Init(InitMessage),
1171 Status(StatusMessage),
1172 CompactBoundary(CompactBoundaryMessage),
1173 ThinkingTokens(ThinkingTokensMessage),
1174 TaskStarted(TaskStartedMessage),
1175 TaskProgress(TaskProgressMessage),
1176 TaskUpdated(TaskUpdatedMessage),
1177 TaskNotification(TaskNotificationMessage),
1178 ApiRetry(ApiRetryMessage),
1179 ControlRequestProgress(ControlRequestProgressMessage),
1180 ModelRefusalFallback(ModelRefusalFallbackMessage),
1181 ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1182 LocalCommandOutput(LocalCommandOutputMessage),
1183 HookStarted(HookStartedMessage),
1184 HookProgress(HookProgressMessage),
1185 HookResponse(HookResponseMessage),
1186 PluginInstall(PluginInstallMessage),
1187 BackgroundTasksChanged(BackgroundTasksChangedMessage),
1188 SessionStateChanged(SessionStateChangedMessage),
1189 WorkerShuttingDown(WorkerShuttingDownMessage),
1190 CommandsChanged(CommandsChangedMessage),
1191 Notification(NotificationMessage),
1192 FilesPersisted(FilesPersistedMessage),
1193 MemoryRecall(MemoryRecallMessage),
1194 ElicitationComplete(ElicitationCompleteMessage),
1195 PermissionDenied(PermissionDeniedMessage),
1196 MirrorError(MirrorErrorMessage),
1197 Informational(InformationalMessage),
1198 CodeChangePublished(CodeChangePublishedMessage),
1199 VcsStateChanged(VcsStateChangedMessage),
1200 FeedbackDraftQueued(FeedbackDraftQueuedMessage),
1201}
1202
1203#[derive(Debug, Clone, Serialize, Deserialize)]
1204pub struct ApiRetryMessage {
1205 pub attempt: u64,
1206 pub max_retries: u64,
1207 pub retry_delay_ms: u64,
1208 pub error_status: Option<u16>,
1209 pub error: String,
1210 #[serde(default, skip_serializing_if = "Option::is_none")]
1211 pub uuid: Option<String>,
1212 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub session_id: Option<String>,
1214}
1215
1216#[derive(Debug, Clone, Serialize, Deserialize)]
1217pub struct ControlRequestProgressMessage {
1218 pub request_id: String,
1219 pub status: String,
1220 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 pub attempt: Option<u64>,
1222 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub max_retries: Option<u64>,
1224 #[serde(default, skip_serializing_if = "Option::is_none")]
1225 pub retry_delay_ms: Option<u64>,
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1227 pub error_status: Option<u16>,
1228 #[serde(default, skip_serializing_if = "Option::is_none")]
1229 pub error: Option<String>,
1230 #[serde(default, skip_serializing_if = "Option::is_none")]
1231 pub uuid: Option<String>,
1232 #[serde(default, skip_serializing_if = "Option::is_none")]
1233 pub session_id: Option<String>,
1234}
1235
1236#[derive(Debug, Clone, Serialize, Deserialize)]
1237pub struct ModelRefusalFallbackMessage {
1238 pub trigger: String,
1239 pub direction: String,
1240 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub scope: Option<RefusalFallbackScope>,
1247 pub original_model: String,
1248 pub fallback_model: String,
1249 pub request_id: Option<String>,
1250 #[serde(default, skip_serializing_if = "Option::is_none")]
1251 pub api_refusal_category: Option<String>,
1252 #[serde(default, skip_serializing_if = "Option::is_none")]
1253 pub api_refusal_explanation: Option<String>,
1254 #[serde(default, skip_serializing_if = "Option::is_none")]
1255 pub retracted_message_uuids: Option<Vec<String>>,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub refused_user_message_uuid: Option<String>,
1258 pub content: Value,
1259 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub uuid: Option<String>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub session_id: Option<String>,
1263}
1264
1265#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1269pub enum RefusalFallbackScope {
1270 Session,
1272 Local,
1275 Unknown(String),
1277}
1278
1279impl RefusalFallbackScope {
1280 pub fn as_str(&self) -> &str {
1281 match self {
1282 Self::Session => "session",
1283 Self::Local => "local",
1284 Self::Unknown(s) => s.as_str(),
1285 }
1286 }
1287}
1288
1289impl fmt::Display for RefusalFallbackScope {
1290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291 f.write_str(self.as_str())
1292 }
1293}
1294
1295impl From<&str> for RefusalFallbackScope {
1296 fn from(s: &str) -> Self {
1297 match s {
1298 "session" => Self::Session,
1299 "local" => Self::Local,
1300 other => Self::Unknown(other.to_string()),
1301 }
1302 }
1303}
1304
1305impl Serialize for RefusalFallbackScope {
1306 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1307 serializer.serialize_str(self.as_str())
1308 }
1309}
1310
1311impl<'de> Deserialize<'de> for RefusalFallbackScope {
1312 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1313 let s = String::deserialize(deserializer)?;
1314 Ok(Self::from(s.as_str()))
1315 }
1316}
1317
1318#[derive(Debug, Clone, Serialize, Deserialize)]
1319pub struct ModelRefusalNoFallbackMessage {
1320 pub original_model: String,
1321 pub request_id: Option<String>,
1322 #[serde(default, skip_serializing_if = "Option::is_none")]
1323 pub api_refusal_category: Option<String>,
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub api_refusal_explanation: Option<String>,
1326 pub content: Value,
1327 #[serde(default, skip_serializing_if = "Option::is_none")]
1328 pub uuid: Option<String>,
1329 #[serde(default, skip_serializing_if = "Option::is_none")]
1330 pub session_id: Option<String>,
1331}
1332
1333#[derive(Debug, Clone, Serialize, Deserialize)]
1334pub struct LocalCommandOutputMessage {
1335 pub content: String,
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 HookStartedMessage {
1344 pub hook_id: String,
1345 pub hook_name: String,
1346 pub hook_event: String,
1347 #[serde(default, skip_serializing_if = "Option::is_none")]
1348 pub uuid: Option<String>,
1349 #[serde(default, skip_serializing_if = "Option::is_none")]
1350 pub session_id: Option<String>,
1351}
1352
1353#[derive(Debug, Clone, Serialize, Deserialize)]
1354pub struct HookProgressMessage {
1355 pub hook_id: String,
1356 pub hook_name: String,
1357 pub hook_event: String,
1358 #[serde(default, skip_serializing_if = "Option::is_none")]
1359 pub stdout: Option<String>,
1360 #[serde(default, skip_serializing_if = "Option::is_none")]
1361 pub stderr: Option<String>,
1362 #[serde(default, skip_serializing_if = "Option::is_none")]
1363 pub output: Option<String>,
1364 #[serde(default, skip_serializing_if = "Option::is_none")]
1365 pub uuid: Option<String>,
1366 #[serde(default, skip_serializing_if = "Option::is_none")]
1367 pub session_id: Option<String>,
1368}
1369
1370#[derive(Debug, Clone, Serialize, Deserialize)]
1371pub struct HookResponseMessage {
1372 pub hook_id: String,
1373 pub hook_name: String,
1374 pub hook_event: String,
1375 #[serde(default, skip_serializing_if = "Option::is_none")]
1376 pub stdout: Option<String>,
1377 #[serde(default, skip_serializing_if = "Option::is_none")]
1378 pub stderr: Option<String>,
1379 #[serde(default, skip_serializing_if = "Option::is_none")]
1380 pub output: Option<String>,
1381 #[serde(default, skip_serializing_if = "Option::is_none")]
1382 pub exit_code: Option<i32>,
1383 pub outcome: String,
1384 #[serde(default, skip_serializing_if = "Option::is_none")]
1385 pub uuid: Option<String>,
1386 #[serde(default, skip_serializing_if = "Option::is_none")]
1387 pub session_id: Option<String>,
1388}
1389
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct PluginInstallMessage {
1392 pub status: String,
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1394 pub name: Option<String>,
1395 #[serde(default, skip_serializing_if = "Option::is_none")]
1396 pub error: Option<String>,
1397 #[serde(default, skip_serializing_if = "Option::is_none")]
1398 pub uuid: Option<String>,
1399 #[serde(default, skip_serializing_if = "Option::is_none")]
1400 pub session_id: Option<String>,
1401}
1402
1403#[derive(Debug, Clone, Serialize, Deserialize)]
1404pub struct BackgroundTasksChangedMessage {
1405 pub tasks: Vec<BackgroundTaskInfo>,
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 BackgroundTaskInfo {
1414 pub task_id: String,
1415 pub task_type: String,
1416 pub description: String,
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct SessionStateChangedMessage {
1421 pub state: String,
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1423 pub uuid: Option<String>,
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1425 pub session_id: Option<String>,
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct WorkerShuttingDownMessage {
1430 pub reason: 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 CommandsChangedMessage {
1439 pub commands: Vec<CommandInfo>,
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 CommandInfo {
1448 pub name: String,
1449 pub description: String,
1450 #[serde(rename = "argumentHint")]
1451 pub argument_hint: String,
1452 #[serde(default, skip_serializing_if = "Option::is_none")]
1453 pub aliases: Option<Vec<String>>,
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1457pub struct NotificationMessage {
1458 pub key: String,
1459 pub text: String,
1460 pub priority: String,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1462 pub color: Option<String>,
1463 #[serde(default, skip_serializing_if = "Option::is_none")]
1464 pub timeout_ms: Option<u64>,
1465 #[serde(default, skip_serializing_if = "Option::is_none")]
1466 pub uuid: Option<String>,
1467 #[serde(default, skip_serializing_if = "Option::is_none")]
1468 pub session_id: Option<String>,
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize)]
1472pub struct FilesPersistedMessage {
1473 pub files: Vec<PersistedFile>,
1474 pub failed: Vec<FailedPersistedFile>,
1475 pub processed_at: String,
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1477 pub uuid: Option<String>,
1478 #[serde(default, skip_serializing_if = "Option::is_none")]
1479 pub session_id: Option<String>,
1480}
1481
1482#[derive(Debug, Clone, Serialize, Deserialize)]
1483pub struct PersistedFile {
1484 pub filename: String,
1485 pub file_id: String,
1486}
1487
1488#[derive(Debug, Clone, Serialize, Deserialize)]
1489pub struct FailedPersistedFile {
1490 pub filename: String,
1491 pub error: String,
1492}
1493
1494#[derive(Debug, Clone, Serialize, Deserialize)]
1495pub struct MemoryRecallMessage {
1496 pub mode: String,
1497 pub memories: Vec<MemoryRecallItem>,
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1499 pub uuid: Option<String>,
1500 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub session_id: Option<String>,
1502}
1503
1504#[derive(Debug, Clone, Serialize, Deserialize)]
1505pub struct MemoryRecallItem {
1506 pub path: String,
1507 pub scope: String,
1508 #[serde(default, skip_serializing_if = "Option::is_none")]
1509 pub content: Option<String>,
1510}
1511
1512#[derive(Debug, Clone, Serialize, Deserialize)]
1513pub struct ElicitationCompleteMessage {
1514 pub mcp_server_name: String,
1515 pub elicitation_id: String,
1516 #[serde(default, skip_serializing_if = "Option::is_none")]
1517 pub uuid: Option<String>,
1518 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub session_id: Option<String>,
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize)]
1523pub struct PermissionDeniedMessage {
1524 pub tool_name: String,
1525 pub tool_use_id: String,
1526 #[serde(default, skip_serializing_if = "Option::is_none")]
1527 pub agent_id: Option<String>,
1528 #[serde(default, skip_serializing_if = "Option::is_none")]
1529 pub decision_reason_type: Option<String>,
1530 #[serde(default, skip_serializing_if = "Option::is_none")]
1531 pub decision_reason: Option<String>,
1532 pub message: String,
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub uuid: Option<String>,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub session_id: Option<String>,
1537}
1538
1539#[derive(Debug, Clone, Serialize, Deserialize)]
1540pub struct MirrorErrorMessage {
1541 pub error: String,
1542 pub key: MirrorErrorKey,
1543 #[serde(default, skip_serializing_if = "Option::is_none")]
1544 pub uuid: Option<String>,
1545 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub session_id: Option<String>,
1547}
1548
1549#[derive(Debug, Clone, Serialize, Deserialize)]
1550pub struct MirrorErrorKey {
1551 #[serde(rename = "projectKey")]
1552 pub project_key: String,
1553 #[serde(rename = "sessionId")]
1554 pub session_id: String,
1555 #[serde(default, skip_serializing_if = "Option::is_none")]
1556 pub subpath: Option<String>,
1557}
1558
1559#[derive(Debug, Clone, Serialize, Deserialize)]
1560pub struct InformationalMessage {
1561 pub content: String,
1562 pub level: String,
1563 #[serde(default, skip_serializing_if = "Option::is_none")]
1564 pub tool_use_id: Option<String>,
1565 #[serde(default, skip_serializing_if = "Option::is_none")]
1566 pub prevent_continuation: Option<bool>,
1567 #[serde(default, skip_serializing_if = "Option::is_none")]
1568 pub uuid: Option<String>,
1569 #[serde(default, skip_serializing_if = "Option::is_none")]
1570 pub session_id: Option<String>,
1571}
1572
1573#[derive(Debug, Clone, Serialize, Deserialize)]
1575pub struct PluginInfo {
1576 pub name: String,
1578 pub path: String,
1580 #[serde(skip_serializing_if = "Option::is_none")]
1582 pub source: Option<String>,
1583 #[serde(default, skip_serializing_if = "Option::is_none")]
1585 pub version: Option<String>,
1586}
1587
1588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1590pub struct PluginDiagnostic {
1591 pub plugin: String,
1592 #[serde(rename = "type")]
1593 pub diagnostic_type: String,
1594 pub message: String,
1595}
1596
1597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1599pub struct MemoryPaths {
1600 #[serde(default, skip_serializing_if = "Option::is_none")]
1601 pub auto: Option<String>,
1602 #[serde(default, skip_serializing_if = "Option::is_none")]
1603 pub team: Option<String>,
1604 #[serde(flatten)]
1605 pub extra: serde_json::Map<String, Value>,
1606}
1607
1608#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1612pub struct McpServerError {
1613 pub name: String,
1614 #[serde(rename = "type")]
1616 pub error_type: String,
1617 pub message: String,
1618}
1619
1620#[derive(Debug, Clone, Serialize, Deserialize)]
1622pub struct InitMessage {
1623 pub session_id: String,
1625 #[serde(skip_serializing_if = "Option::is_none")]
1627 pub cwd: Option<String>,
1628 #[serde(skip_serializing_if = "Option::is_none")]
1630 pub model: Option<String>,
1631 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1633 pub tools: Vec<String>,
1634 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1636 pub mcp_servers: Vec<Value>,
1637 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1639 pub slash_commands: Vec<String>,
1640 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1643 pub terminal_slash_commands: Vec<String>,
1644 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1646 pub agents: Vec<String>,
1647 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1649 pub plugins: Vec<PluginInfo>,
1650 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1652 pub skills: Vec<Value>,
1653 #[serde(skip_serializing_if = "Option::is_none")]
1655 pub claude_code_version: Option<String>,
1656 #[serde(skip_serializing_if = "Option::is_none")]
1659 pub messaging_socket_path: Option<String>,
1660 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1662 pub api_key_source: Option<ApiKeySource>,
1663 #[serde(skip_serializing_if = "Option::is_none")]
1665 pub output_style: Option<OutputStyle>,
1666 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1668 pub permission_mode: Option<InitPermissionMode>,
1669
1670 #[serde(skip_serializing_if = "Option::is_none")]
1672 pub uuid: Option<String>,
1673
1674 #[serde(skip_serializing_if = "Option::is_none")]
1676 pub memory_paths: Option<MemoryPaths>,
1677
1678 #[serde(skip_serializing_if = "Option::is_none")]
1680 pub fast_mode_state: Option<String>,
1681
1682 #[serde(default, skip_serializing_if = "Option::is_none")]
1684 pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
1685
1686 #[serde(default, skip_serializing_if = "Option::is_none")]
1689 pub mcp_server_errors: Option<Vec<McpServerError>>,
1690
1691 #[serde(default, skip_serializing_if = "Option::is_none")]
1693 pub analytics_disabled: Option<bool>,
1694
1695 #[serde(default, skip_serializing_if = "Option::is_none")]
1697 pub product_feedback_disabled: Option<bool>,
1698
1699 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1701 pub betas: Vec<String>,
1702
1703 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1705 pub capabilities: Vec<String>,
1706
1707 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1709 pub plugin_errors: Vec<PluginDiagnostic>,
1710
1711 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1713 pub plugin_warnings: Vec<PluginDiagnostic>,
1714}
1715
1716#[derive(Debug, Clone, Serialize, Deserialize)]
1718pub struct StatusMessage {
1719 pub session_id: String,
1721 pub status: Option<StatusMessageStatus>,
1723 #[serde(skip_serializing_if = "Option::is_none")]
1725 pub uuid: Option<String>,
1726 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1728 pub permission_mode: Option<InitPermissionMode>,
1729 #[serde(skip_serializing_if = "Option::is_none")]
1730 pub compact_result: Option<String>,
1731 #[serde(skip_serializing_if = "Option::is_none")]
1732 pub compact_error: Option<String>,
1733}
1734
1735#[derive(Debug, Clone, Serialize, Deserialize)]
1737pub struct CompactBoundaryMessage {
1738 pub session_id: String,
1740 pub compact_metadata: CompactMetadata,
1742 #[serde(
1746 default,
1747 skip_serializing_if = "Option::is_none",
1748 alias = "content",
1749 alias = "text"
1750 )]
1751 pub summary: Option<String>,
1752 #[serde(
1756 default,
1757 skip_serializing_if = "Option::is_none",
1758 alias = "message_count"
1759 )]
1760 pub leaf_message_count: Option<u32>,
1761 #[serde(default, skip_serializing_if = "Option::is_none")]
1763 pub duration_ms: Option<u64>,
1764 #[serde(skip_serializing_if = "Option::is_none")]
1766 pub uuid: Option<String>,
1767 #[serde(skip_serializing_if = "Option::is_none")]
1769 pub logical_parent_uuid: Option<Option<String>>,
1770}
1771
1772#[derive(Debug, Clone, Serialize, Deserialize)]
1774pub struct CompactMetadata {
1775 pub pre_tokens: u64,
1777 pub trigger: CompactionTrigger,
1779 #[serde(default, skip_serializing_if = "Option::is_none")]
1780 pub post_tokens: Option<u64>,
1781 #[serde(default, skip_serializing_if = "Option::is_none")]
1782 pub cumulative_dropped_tokens: Option<u64>,
1783 #[serde(default, skip_serializing_if = "Option::is_none")]
1784 pub duration_ms: Option<u64>,
1785 #[serde(default, skip_serializing_if = "Option::is_none")]
1786 pub user_context: Option<String>,
1787 #[serde(default, skip_serializing_if = "Option::is_none")]
1788 pub messages_summarized: Option<u64>,
1789 #[serde(default, skip_serializing_if = "Option::is_none")]
1790 pub precomputed: Option<bool>,
1791 #[serde(default, skip_serializing_if = "Option::is_none")]
1792 pub pre_compact_discovered_tools: Option<Vec<String>>,
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1794 pub preserved_segment: Option<PreservedSegment>,
1795 #[serde(default, skip_serializing_if = "Option::is_none")]
1796 pub preserved_messages: Option<PreservedMessages>,
1797}
1798
1799#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1800pub struct PreservedSegment {
1801 pub head_uuid: String,
1802 pub anchor_uuid: String,
1803 pub tail_uuid: String,
1804}
1805
1806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1807pub struct PreservedMessages {
1808 pub anchor_uuid: String,
1809 pub uuids: Vec<String>,
1810 #[serde(default, skip_serializing_if = "Option::is_none")]
1811 pub all_uuids: Option<Vec<String>>,
1812}
1813
1814#[derive(Debug, Clone, Serialize, Deserialize)]
1820pub struct TaskUsage {
1821 pub duration_ms: u64,
1823 pub tool_uses: u64,
1825 pub total_tokens: u64,
1827}
1828
1829#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1831pub enum TaskType {
1832 LocalAgent,
1834 LocalBash,
1836 LocalWorkflow,
1838 Unknown(String),
1840}
1841
1842impl TaskType {
1843 pub fn as_str(&self) -> &str {
1844 match self {
1845 Self::LocalAgent => "local_agent",
1846 Self::LocalBash => "local_bash",
1847 Self::LocalWorkflow => "local_workflow",
1848 Self::Unknown(s) => s.as_str(),
1849 }
1850 }
1851}
1852
1853impl fmt::Display for TaskType {
1854 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1855 f.write_str(self.as_str())
1856 }
1857}
1858
1859impl From<&str> for TaskType {
1860 fn from(s: &str) -> Self {
1861 match s {
1862 "local_agent" => Self::LocalAgent,
1863 "local_bash" => Self::LocalBash,
1864 "local_workflow" => Self::LocalWorkflow,
1865 other => Self::Unknown(other.to_string()),
1866 }
1867 }
1868}
1869
1870impl Serialize for TaskType {
1871 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1872 serializer.serialize_str(self.as_str())
1873 }
1874}
1875
1876impl<'de> Deserialize<'de> for TaskType {
1877 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1878 let s = String::deserialize(deserializer)?;
1879 Ok(Self::from(s.as_str()))
1880 }
1881}
1882
1883#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1885pub enum TaskStatus {
1886 Pending,
1887 Running,
1888 Completed,
1889 Failed,
1890 Killed,
1891 Paused,
1892 Stopped,
1893 Unknown(String),
1894}
1895
1896impl TaskStatus {
1897 pub fn as_str(&self) -> &str {
1898 match self {
1899 Self::Pending => "pending",
1900 Self::Running => "running",
1901 Self::Completed => "completed",
1902 Self::Failed => "failed",
1903 Self::Killed => "killed",
1904 Self::Paused => "paused",
1905 Self::Stopped => "stopped",
1906 Self::Unknown(s) => s.as_str(),
1907 }
1908 }
1909}
1910
1911impl fmt::Display for TaskStatus {
1912 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1913 f.write_str(self.as_str())
1914 }
1915}
1916
1917impl From<&str> for TaskStatus {
1918 fn from(s: &str) -> Self {
1919 match s {
1920 "pending" => Self::Pending,
1921 "running" => Self::Running,
1922 "completed" => Self::Completed,
1923 "failed" => Self::Failed,
1924 "killed" => Self::Killed,
1925 "paused" => Self::Paused,
1926 "stopped" => Self::Stopped,
1927 other => Self::Unknown(other.to_string()),
1928 }
1929 }
1930}
1931
1932impl Serialize for TaskStatus {
1933 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1934 serializer.serialize_str(self.as_str())
1935 }
1936}
1937
1938impl<'de> Deserialize<'de> for TaskStatus {
1939 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1940 let s = String::deserialize(deserializer)?;
1941 Ok(Self::from(s.as_str()))
1942 }
1943}
1944
1945#[derive(Debug, Clone, Serialize, Deserialize)]
1947pub struct TaskStartedMessage {
1948 pub session_id: String,
1949 pub task_id: String,
1950 #[serde(default, skip_serializing_if = "Option::is_none")]
1951 pub task_type: Option<TaskType>,
1952 #[serde(default, skip_serializing_if = "Option::is_none")]
1953 pub tool_use_id: Option<String>,
1954 pub description: String,
1955 #[serde(default, skip_serializing_if = "Option::is_none")]
1958 pub subagent_type: Option<String>,
1959 #[serde(default, skip_serializing_if = "Option::is_none")]
1961 pub prompt: Option<String>,
1962 #[serde(default, skip_serializing_if = "Option::is_none")]
1963 pub workflow_name: Option<String>,
1964 #[serde(default, skip_serializing_if = "Option::is_none")]
1965 pub skip_transcript: Option<bool>,
1966 pub uuid: String,
1967}
1968
1969#[derive(Debug, Clone, Serialize, Deserialize)]
1973pub struct TaskUpdatedMessage {
1974 pub session_id: String,
1975 pub task_id: String,
1976 pub patch: TaskPatch,
1977 pub uuid: String,
1978}
1979
1980#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1983pub struct TaskPatch {
1984 #[serde(default, skip_serializing_if = "Option::is_none")]
1985 pub status: Option<TaskStatus>,
1986 #[serde(default, skip_serializing_if = "Option::is_none")]
1989 pub end_time: Option<u64>,
1990 #[serde(default, skip_serializing_if = "Option::is_none")]
1991 pub description: Option<String>,
1992 #[serde(default, skip_serializing_if = "Option::is_none")]
1993 pub total_paused_ms: Option<u64>,
1994 #[serde(default, skip_serializing_if = "Option::is_none")]
1995 pub error: Option<String>,
1996 #[serde(default, skip_serializing_if = "Option::is_none")]
1997 pub is_backgrounded: Option<bool>,
1998}
1999
2000#[derive(Debug, Clone, Serialize, Deserialize)]
2003pub struct ThinkingTokensMessage {
2004 pub session_id: String,
2005 pub estimated_tokens: u64,
2007 pub estimated_tokens_delta: u64,
2009 pub uuid: String,
2010}
2011
2012#[derive(Debug, Clone, Serialize, Deserialize)]
2015pub struct TaskProgressMessage {
2016 pub session_id: String,
2017 pub task_id: String,
2018 #[serde(default, skip_serializing_if = "Option::is_none")]
2019 pub tool_use_id: Option<String>,
2020 pub description: String,
2021 #[serde(default, skip_serializing_if = "Option::is_none")]
2022 pub last_tool_name: Option<String>,
2023 pub usage: TaskUsage,
2024 #[serde(default, skip_serializing_if = "Option::is_none")]
2026 pub subagent_type: Option<String>,
2027 #[serde(default, skip_serializing_if = "Option::is_none")]
2028 pub summary: Option<String>,
2029 pub uuid: String,
2030}
2031
2032#[derive(Debug, Clone, Serialize, Deserialize)]
2035pub struct TaskNotificationMessage {
2036 pub session_id: String,
2037 pub task_id: String,
2038 pub status: TaskStatus,
2039 pub summary: String,
2040 pub output_file: Option<String>,
2041 #[serde(skip_serializing_if = "Option::is_none")]
2042 pub tool_use_id: Option<String>,
2043 #[serde(skip_serializing_if = "Option::is_none")]
2044 pub usage: Option<TaskUsage>,
2045 #[serde(default, skip_serializing_if = "Option::is_none")]
2046 pub skip_transcript: Option<bool>,
2047 #[serde(skip_serializing_if = "Option::is_none")]
2048 pub uuid: Option<String>,
2049}
2050
2051#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2053pub enum AssistantErrorKind {
2054 AuthenticationFailed,
2055 OauthOrgNotAllowed,
2056 BillingError,
2057 RateLimit,
2058 Overloaded,
2059 InvalidRequest,
2060 ModelNotFound,
2061 ServerError,
2062 UnknownError,
2063 MaxOutputTokens,
2064 Unknown(String),
2065}
2066
2067impl AssistantErrorKind {
2068 pub fn as_str(&self) -> &str {
2069 match self {
2070 Self::AuthenticationFailed => "authentication_failed",
2071 Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
2072 Self::BillingError => "billing_error",
2073 Self::RateLimit => "rate_limit",
2074 Self::Overloaded => "overloaded",
2075 Self::InvalidRequest => "invalid_request",
2076 Self::ModelNotFound => "model_not_found",
2077 Self::ServerError => "server_error",
2078 Self::UnknownError => "unknown",
2079 Self::MaxOutputTokens => "max_output_tokens",
2080 Self::Unknown(s) => s.as_str(),
2081 }
2082 }
2083}
2084
2085impl fmt::Display for AssistantErrorKind {
2086 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2087 f.write_str(self.as_str())
2088 }
2089}
2090
2091impl From<&str> for AssistantErrorKind {
2092 fn from(s: &str) -> Self {
2093 match s {
2094 "authentication_failed" => Self::AuthenticationFailed,
2095 "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
2096 "billing_error" => Self::BillingError,
2097 "rate_limit" => Self::RateLimit,
2098 "overloaded" => Self::Overloaded,
2099 "invalid_request" => Self::InvalidRequest,
2100 "model_not_found" => Self::ModelNotFound,
2101 "server_error" => Self::ServerError,
2102 "unknown" => Self::UnknownError,
2103 "max_output_tokens" => Self::MaxOutputTokens,
2104 other => Self::Unknown(other.to_string()),
2105 }
2106 }
2107}
2108
2109impl Serialize for AssistantErrorKind {
2110 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2111 serializer.serialize_str(self.as_str())
2112 }
2113}
2114
2115impl<'de> Deserialize<'de> for AssistantErrorKind {
2116 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2117 let s = String::deserialize(deserializer)?;
2118 Ok(Self::from(s.as_str()))
2119 }
2120}
2121
2122#[derive(Debug, Clone, Serialize, Deserialize)]
2129pub struct CodeChangePublishedMessage {
2130 pub provider: String,
2134 pub url: String,
2136 pub repo: String,
2139 pub identifier: String,
2141 pub uuid: String,
2142 pub session_id: String,
2143}
2144
2145#[derive(Debug, Clone, Serialize, Deserialize)]
2150pub struct VcsStateChangedMessage {
2151 pub kind: VcsMutationKind,
2154 pub cwd: String,
2157 pub uuid: String,
2158 pub session_id: String,
2159}
2160
2161#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2163pub enum VcsMutationKind {
2164 Commit,
2165 Push,
2166 Merge,
2167 Rebase,
2168 Unknown(String),
2170}
2171
2172impl VcsMutationKind {
2173 pub fn as_str(&self) -> &str {
2174 match self {
2175 Self::Commit => "commit",
2176 Self::Push => "push",
2177 Self::Merge => "merge",
2178 Self::Rebase => "rebase",
2179 Self::Unknown(s) => s.as_str(),
2180 }
2181 }
2182}
2183
2184impl fmt::Display for VcsMutationKind {
2185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2186 f.write_str(self.as_str())
2187 }
2188}
2189
2190impl From<&str> for VcsMutationKind {
2191 fn from(s: &str) -> Self {
2192 match s {
2193 "commit" => Self::Commit,
2194 "push" => Self::Push,
2195 "merge" => Self::Merge,
2196 "rebase" => Self::Rebase,
2197 other => Self::Unknown(other.to_string()),
2198 }
2199 }
2200}
2201
2202impl Serialize for VcsMutationKind {
2203 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2204 serializer.serialize_str(self.as_str())
2205 }
2206}
2207
2208impl<'de> Deserialize<'de> for VcsMutationKind {
2209 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2210 let s = String::deserialize(deserializer)?;
2211 Ok(Self::from(s.as_str()))
2212 }
2213}
2214
2215#[derive(Debug, Clone, Serialize, Deserialize)]
2217pub struct FeedbackDraftQueuedMessage {
2218 pub draft_id: String,
2219 pub draft_type: String,
2220 pub title: String,
2221 pub details_preview: String,
2222 #[serde(default, skip_serializing_if = "Option::is_none")]
2223 pub uuid: Option<String>,
2224 #[serde(default, skip_serializing_if = "Option::is_none")]
2225 pub session_id: Option<String>,
2226 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2227 pub extra: serde_json::Map<String, Value>,
2228}
2229
2230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2232pub struct ToolUseMeta {
2233 pub id: String,
2234 pub display_name: String,
2235 #[serde(default, skip_serializing_if = "Option::is_none")]
2236 pub server_display_name: Option<String>,
2237 #[serde(default, skip_serializing_if = "Option::is_none")]
2238 pub icon_url: Option<String>,
2239}
2240
2241#[derive(Debug, Clone, Serialize, Deserialize)]
2243pub struct AssistantMessage {
2244 pub message: AssistantMessageContent,
2245 #[serde(alias = "sessionId")]
2246 pub session_id: String,
2247 #[serde(skip_serializing_if = "Option::is_none")]
2248 pub uuid: Option<String>,
2249 #[serde(skip_serializing_if = "Option::is_none")]
2250 pub parent_tool_use_id: Option<String>,
2251 #[serde(skip_serializing_if = "Option::is_none")]
2253 pub request_id: Option<String>,
2254 #[serde(skip_serializing_if = "Option::is_none")]
2257 pub subagent_type: Option<String>,
2258 #[serde(skip_serializing_if = "Option::is_none")]
2260 pub task_description: Option<String>,
2261 #[serde(skip_serializing_if = "Option::is_none")]
2262 pub error: Option<AssistantErrorKind>,
2263 #[serde(default, skip_serializing_if = "Option::is_none")]
2267 pub aborted: Option<bool>,
2268 #[serde(default, skip_serializing_if = "Option::is_none")]
2273 pub resumed_from_incomplete_thinking: Option<bool>,
2274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2275 pub supersedes: Vec<String>,
2276 #[serde(skip_serializing_if = "Option::is_none")]
2277 pub timestamp: Option<String>,
2278 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2279 pub tool_use_meta: Vec<ToolUseMeta>,
2280 #[serde(default, skip_serializing_if = "Option::is_none")]
2281 pub is_meta: Option<bool>,
2282 #[serde(default, skip_serializing_if = "Option::is_none")]
2283 pub is_virtual: Option<bool>,
2284 #[serde(default, skip_serializing_if = "Option::is_none")]
2285 pub is_api_error_message: Option<bool>,
2286 #[serde(skip_serializing_if = "Option::is_none")]
2287 pub api_error_status: Option<u16>,
2288 #[serde(skip_serializing_if = "Option::is_none")]
2289 pub api_error: Option<String>,
2290 #[serde(skip_serializing_if = "Option::is_none")]
2291 pub error_details: Option<String>,
2292 #[serde(skip_serializing_if = "Option::is_none")]
2293 pub advisor_model: Option<String>,
2294 #[serde(skip_serializing_if = "Option::is_none")]
2295 pub attribution_agent: Option<String>,
2296 #[serde(skip_serializing_if = "Option::is_none")]
2297 pub attribution_skill: Option<String>,
2298 #[serde(skip_serializing_if = "Option::is_none")]
2299 pub attribution_plugin: Option<String>,
2300 #[serde(skip_serializing_if = "Option::is_none")]
2301 pub attribution_mcp_server: Option<String>,
2302 #[serde(skip_serializing_if = "Option::is_none")]
2303 pub attribution_mcp_tool: Option<String>,
2304}
2305
2306#[derive(Debug, Clone, Serialize, Deserialize)]
2308pub struct AssistantMessageContent {
2309 pub id: String,
2310 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2312 pub message_type: Option<String>,
2313 pub role: MessageRole,
2314 pub model: String,
2315 pub content: Vec<ContentBlock>,
2316 #[serde(skip_serializing_if = "Option::is_none")]
2317 pub stop_reason: Option<StopReason>,
2318 #[serde(skip_serializing_if = "Option::is_none")]
2319 pub stop_sequence: Option<String>,
2320 #[serde(skip_serializing_if = "Option::is_none")]
2321 pub usage: Option<AssistantUsage>,
2322 #[serde(skip_serializing_if = "Option::is_none")]
2324 pub stop_details: Option<Value>,
2325 #[serde(skip_serializing_if = "Option::is_none")]
2327 pub context_management: Option<Value>,
2328}
2329
2330#[derive(Debug, Clone, Serialize, Deserialize)]
2332pub struct AssistantUsage {
2333 #[serde(default)]
2335 pub input_tokens: u32,
2336
2337 #[serde(default)]
2339 pub output_tokens: u32,
2340
2341 #[serde(default)]
2343 pub cache_creation_input_tokens: u32,
2344
2345 #[serde(default)]
2347 pub cache_read_input_tokens: u32,
2348
2349 #[serde(skip_serializing_if = "Option::is_none")]
2351 pub service_tier: Option<String>,
2352
2353 #[serde(skip_serializing_if = "Option::is_none")]
2355 pub cache_creation: Option<CacheCreationDetails>,
2356
2357 #[serde(skip_serializing_if = "Option::is_none")]
2359 pub inference_geo: Option<String>,
2360}
2361
2362#[derive(Debug, Clone, Serialize, Deserialize)]
2364pub struct CacheCreationDetails {
2365 #[serde(default)]
2367 pub ephemeral_1h_input_tokens: u32,
2368
2369 #[serde(default)]
2371 pub ephemeral_5m_input_tokens: u32,
2372}
2373
2374#[cfg(test)]
2375mod tests {
2376 use crate::io::ClaudeOutput;
2377
2378 #[test]
2379 fn test_subagent_usage_rollup_accumulates_task_results() {
2380 use super::SubagentUsageRollup;
2381
2382 let mut rollup = SubagentUsageRollup::default();
2383
2384 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}}"#;
2385 let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2386 assert!(rollup.observe(&output));
2387 assert_eq!(rollup.subagent_tokens, 10201);
2388 assert_eq!(rollup.agent_count, 1);
2389 assert_eq!(rollup.tool_uses, 3);
2390 assert_eq!(rollup.duration_ms, 1853);
2391
2392 assert!(!rollup.observe(&output));
2394 assert_eq!(rollup.agent_count, 1);
2395 assert_eq!(rollup.subagent_tokens, 10201);
2396
2397 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}}"#;
2399 let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2400 assert!(rollup.observe(&output));
2401 assert_eq!(rollup.agent_count, 2);
2402 assert_eq!(rollup.subagent_tokens, 10701);
2403 }
2404
2405 #[test]
2406 fn test_subagent_usage_rollup_ignores_non_task_results() {
2407 use super::SubagentUsageRollup;
2408
2409 let mut rollup = SubagentUsageRollup::default();
2410
2411 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}}"#;
2414 let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2415 assert!(!rollup.observe(&output));
2416
2417 let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2419 let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2420 assert!(!rollup.observe(&output));
2421
2422 let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2424 let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2425 assert!(!rollup.observe(&output));
2426
2427 assert_eq!(rollup, SubagentUsageRollup::default());
2428 }
2429
2430 #[test]
2431 fn test_subagent_usage_rollup_over_captured_session() {
2432 use super::SubagentUsageRollup;
2433
2434 let mut rollup = SubagentUsageRollup::default();
2435 let fixture =
2436 include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2437 for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2438 if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2439 rollup.observe(&output);
2440 }
2441 }
2442 assert_eq!(rollup.agent_count, 1);
2443 assert_eq!(rollup.subagent_tokens, 10201);
2444 }
2445
2446 #[test]
2447 fn test_system_message_init() {
2448 let json = r#"{
2449 "type": "system",
2450 "subtype": "init",
2451 "session_id": "test-session-123",
2452 "cwd": "/home/user/project",
2453 "model": "claude-sonnet-4",
2454 "tools": ["Bash", "Read", "Write"],
2455 "mcp_servers": [],
2456 "slash_commands": ["compact", "cost", "review"],
2457 "agents": ["Bash", "Explore", "Plan"],
2458 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2459 "skills": [],
2460 "claude_code_version": "2.1.15",
2461 "apiKeySource": "none",
2462 "output_style": "default",
2463 "permissionMode": "default"
2464 }"#;
2465
2466 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2467 if let ClaudeOutput::System(sys) = output {
2468 assert!(sys.is_init());
2469 assert!(!sys.is_status());
2470 assert!(!sys.is_compact_boundary());
2471
2472 let init = sys.as_init().expect("Should parse as init");
2473 assert_eq!(init.session_id, "test-session-123");
2474 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2475 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2476 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2477 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2478 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2479 assert_eq!(init.plugins.len(), 1);
2480 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2481 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2482 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2483 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2484 assert_eq!(
2485 init.permission_mode,
2486 Some(super::InitPermissionMode::Default)
2487 );
2488 } else {
2489 panic!("Expected System message");
2490 }
2491 }
2492
2493 #[test]
2494 fn test_system_message_init_from_real_capture() {
2495 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2496 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2497 if let ClaudeOutput::System(sys) = output {
2498 let init = sys.as_init().expect("Should parse real init capture");
2499 assert_eq!(init.slash_commands.len(), 8);
2500 assert!(init.slash_commands.contains(&"compact".to_string()));
2501 assert!(init.slash_commands.contains(&"review".to_string()));
2502 assert_eq!(init.agents.len(), 5);
2503 assert!(init.agents.contains(&"Bash".to_string()));
2504 assert!(init.agents.contains(&"Explore".to_string()));
2505 assert_eq!(init.plugins.len(), 1);
2506 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2507 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2508 } else {
2509 panic!("Expected System message");
2510 }
2511 }
2512
2513 #[test]
2514 fn test_system_message_status() {
2515 let json = r#"{
2516 "type": "system",
2517 "subtype": "status",
2518 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2519 "status": "compacting",
2520 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2521 }"#;
2522
2523 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2524 if let ClaudeOutput::System(sys) = output {
2525 assert!(sys.is_status());
2526 assert!(!sys.is_init());
2527
2528 let status = sys.as_status().expect("Should parse as status");
2529 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2530 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
2531 assert_eq!(
2532 status.uuid,
2533 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2534 );
2535 } else {
2536 panic!("Expected System message");
2537 }
2538 }
2539
2540 #[test]
2541 fn test_system_message_status_null() {
2542 let json = r#"{
2543 "type": "system",
2544 "subtype": "status",
2545 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2546 "status": null,
2547 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2548 }"#;
2549
2550 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2551 if let ClaudeOutput::System(sys) = output {
2552 let status = sys.as_status().expect("Should parse as status");
2553 assert_eq!(status.status, None);
2554 } else {
2555 panic!("Expected System message");
2556 }
2557 }
2558
2559 #[test]
2560 fn test_system_message_task_started() {
2561 let json = r#"{
2562 "type": "system",
2563 "subtype": "task_started",
2564 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2565 "task_id": "b6daf3f",
2566 "task_type": "local_bash",
2567 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
2568 "description": "Wait for CI on PR #12",
2569 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
2570 }"#;
2571
2572 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2573 if let ClaudeOutput::System(sys) = output {
2574 assert!(sys.is_task_started());
2575 assert!(!sys.is_task_progress());
2576 assert!(!sys.is_task_notification());
2577
2578 let task = sys.as_task_started().expect("Should parse as task_started");
2579 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
2580 assert_eq!(task.task_id, "b6daf3f");
2581 assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
2582 assert_eq!(
2583 task.tool_use_id.as_deref(),
2584 Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
2585 );
2586 assert_eq!(task.description, "Wait for CI on PR #12");
2587 } else {
2588 panic!("Expected System message");
2589 }
2590 }
2591
2592 #[test]
2593 fn test_system_message_task_started_agent() {
2594 let json = r#"{
2595 "type": "system",
2596 "subtype": "task_started",
2597 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2598 "task_id": "a4a7e0906e5fc64cc",
2599 "task_type": "local_agent",
2600 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2601 "description": "Explore Scene/ArrayScene duplication",
2602 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2603 }"#;
2604
2605 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2606 if let ClaudeOutput::System(sys) = output {
2607 let task = sys.as_task_started().expect("Should parse as task_started");
2608 assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
2609 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
2610 } else {
2611 panic!("Expected System message");
2612 }
2613 }
2614
2615 #[test]
2616 fn test_system_message_task_progress() {
2617 let json = r#"{
2618 "type": "system",
2619 "subtype": "task_progress",
2620 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2621 "task_id": "a4a7e0906e5fc64cc",
2622 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2623 "description": "Reading src/jplephem/chebyshev.rs",
2624 "last_tool_name": "Read",
2625 "usage": {
2626 "duration_ms": 13996,
2627 "tool_uses": 9,
2628 "total_tokens": 38779
2629 },
2630 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2631 }"#;
2632
2633 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2634 if let ClaudeOutput::System(sys) = output {
2635 assert!(sys.is_task_progress());
2636 assert!(!sys.is_task_started());
2637
2638 let progress = sys
2639 .as_task_progress()
2640 .expect("Should parse as task_progress");
2641 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
2642 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
2643 assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
2644 assert_eq!(progress.usage.duration_ms, 13996);
2645 assert_eq!(progress.usage.tool_uses, 9);
2646 assert_eq!(progress.usage.total_tokens, 38779);
2647 } else {
2648 panic!("Expected System message");
2649 }
2650 }
2651
2652 #[test]
2653 fn test_system_message_task_notification_completed() {
2654 let json = r#"{
2655 "type": "system",
2656 "subtype": "task_notification",
2657 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2658 "task_id": "a0ba761e9dc9c316f",
2659 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
2660 "status": "completed",
2661 "summary": "Agent \"Write Hipparcos data source doc\" completed",
2662 "output_file": "",
2663 "usage": {
2664 "duration_ms": 172300,
2665 "tool_uses": 11,
2666 "total_tokens": 42005
2667 },
2668 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
2669 }"#;
2670
2671 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2672 if let ClaudeOutput::System(sys) = output {
2673 assert!(sys.is_task_notification());
2674
2675 let notif = sys
2676 .as_task_notification()
2677 .expect("Should parse as task_notification");
2678 assert_eq!(notif.status, super::TaskStatus::Completed);
2679 assert_eq!(
2680 notif.summary,
2681 "Agent \"Write Hipparcos data source doc\" completed"
2682 );
2683 assert_eq!(notif.output_file, Some("".to_string()));
2684 assert_eq!(
2685 notif.tool_use_id,
2686 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
2687 );
2688 let usage = notif.usage.expect("Should have usage");
2689 assert_eq!(usage.duration_ms, 172300);
2690 assert_eq!(usage.tool_uses, 11);
2691 assert_eq!(usage.total_tokens, 42005);
2692 } else {
2693 panic!("Expected System message");
2694 }
2695 }
2696
2697 #[test]
2698 fn test_system_message_task_notification_failed_no_usage() {
2699 let json = r#"{
2700 "type": "system",
2701 "subtype": "task_notification",
2702 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
2703 "task_id": "b98f6a3",
2704 "status": "failed",
2705 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
2706 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
2707 }"#;
2708
2709 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2710 if let ClaudeOutput::System(sys) = output {
2711 let notif = sys
2712 .as_task_notification()
2713 .expect("Should parse as task_notification");
2714 assert_eq!(notif.status, super::TaskStatus::Failed);
2715 assert!(notif.tool_use_id.is_none());
2716 assert!(notif.usage.is_none());
2717 assert_eq!(
2718 notif.output_file,
2719 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
2720 );
2721 } else {
2722 panic!("Expected System message");
2723 }
2724 }
2725
2726 #[test]
2732 fn test_task_messages_roundtrip_through_value() {
2733 let cases = [
2734 r#"{"type":"system","subtype":"task_started","session_id":"s1",
2735 "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
2736 "description":"Sleep 3s","uuid":"u1"}"#,
2737 r#"{"type":"system","subtype":"task_progress","session_id":"s1",
2738 "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
2739 "last_tool_name":"Bash",
2740 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2741 "uuid":"u2"}"#,
2742 r#"{"type":"system","subtype":"task_notification","session_id":"s1",
2743 "task_id":"t1","tool_use_id":"tu1","status":"completed",
2744 "summary":"done","output_file":"",
2745 "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2746 "uuid":"u3"}"#,
2747 ];
2748
2749 for json in cases {
2750 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2751 let value = serde_json::to_value(&output).unwrap();
2752 let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
2753
2754 let ClaudeOutput::System(sys) = reparsed else {
2755 panic!("Expected System variant after round-trip");
2756 };
2757
2758 match sys.subtype {
2759 super::SystemSubtype::TaskStarted => {
2760 assert!(
2761 sys.as_task_started().is_some(),
2762 "as_task_started failed after round-trip"
2763 );
2764 }
2765 super::SystemSubtype::TaskProgress => {
2766 assert!(
2767 sys.as_task_progress().is_some(),
2768 "as_task_progress failed after round-trip"
2769 );
2770 }
2771 super::SystemSubtype::TaskNotification => {
2772 assert!(
2773 sys.as_task_notification().is_some(),
2774 "as_task_notification failed after round-trip"
2775 );
2776 }
2777 other => panic!("unexpected subtype after round-trip: {other:?}"),
2778 }
2779 }
2780 }
2781
2782 #[test]
2783 fn test_system_message_compact_boundary() {
2784 let json = r#"{
2785 "type": "system",
2786 "subtype": "compact_boundary",
2787 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2788 "compact_metadata": {
2789 "pre_tokens": 155285,
2790 "trigger": "auto"
2791 },
2792 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
2793 }"#;
2794
2795 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2796 if let ClaudeOutput::System(sys) = output {
2797 assert!(sys.is_compact_boundary());
2798 assert!(!sys.is_init());
2799 assert!(!sys.is_status());
2800
2801 let compact = sys
2802 .as_compact_boundary()
2803 .expect("Should parse as compact_boundary");
2804 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2805 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
2806 assert_eq!(
2807 compact.compact_metadata.trigger,
2808 super::CompactionTrigger::Auto
2809 );
2810 assert!(compact.summary.is_none());
2812 assert!(compact.leaf_message_count.is_none());
2813 assert!(compact.duration_ms.is_none());
2814 } else {
2815 panic!("Expected System message");
2816 }
2817 }
2818
2819 #[test]
2820 fn test_compact_boundary_with_summary_stats() {
2821 let json = r#"{
2823 "type": "system",
2824 "subtype": "compact_boundary",
2825 "session_id": "s1",
2826 "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
2827 "summary": "Summarized the earlier exploration.",
2828 "leaf_message_count": 42,
2829 "duration_ms": 1234,
2830 "uuid": "u1"
2831 }"#;
2832 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2833 let ClaudeOutput::System(sys) = output else {
2834 panic!("Expected System message");
2835 };
2836 let compact = sys.as_compact_boundary().expect("compact_boundary");
2837 assert_eq!(
2838 compact.summary.as_deref(),
2839 Some("Summarized the earlier exploration.")
2840 );
2841 assert_eq!(compact.leaf_message_count, Some(42));
2842 assert_eq!(compact.duration_ms, Some(1234));
2843
2844 let json_alt = r#"{
2847 "type": "system",
2848 "subtype": "compact_boundary",
2849 "session_id": "s2",
2850 "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
2851 "content": "alt-key summary",
2852 "message_count": 7
2853 }"#;
2854 let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
2855 let ClaudeOutput::System(sys) = output else {
2856 panic!("Expected System message");
2857 };
2858 let compact = sys.as_compact_boundary().expect("compact_boundary");
2859 assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
2860 assert_eq!(compact.leaf_message_count, Some(7));
2861 }
2862
2863 #[test]
2864 fn test_init_message_with_new_fields() {
2865 let json = r#"{
2866 "type": "system",
2867 "subtype": "init",
2868 "session_id": "test-session",
2869 "cwd": "/home/user",
2870 "model": "claude-opus-4-7",
2871 "tools": ["Bash"],
2872 "mcp_servers": [],
2873 "permissionMode": "default",
2874 "apiKeySource": "none",
2875 "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
2876 "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
2877 "fast_mode_state": "off",
2878 "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
2879 "claude_code_version": "2.1.117"
2880 }"#;
2881
2882 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2883 if let ClaudeOutput::System(sys) = output {
2884 let init = sys.as_init().expect("Should parse as init");
2885 assert_eq!(
2886 init.uuid.as_deref(),
2887 Some("44841a0d-182d-493a-86b5-79800d3d9665")
2888 );
2889 assert!(init.memory_paths.is_some());
2890 assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
2891 assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
2892 assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
2893 } else {
2894 panic!("Expected System message");
2895 }
2896 }
2897
2898 #[test]
2899 fn test_assistant_message_with_new_fields() {
2900 let json = r#"{
2901 "type": "assistant",
2902 "message": {
2903 "id": "msg_1",
2904 "type": "message",
2905 "role": "assistant",
2906 "model": "claude-opus-4-7",
2907 "content": [{"type": "text", "text": "Hello"}],
2908 "stop_reason": "end_turn",
2909 "stop_details": null,
2910 "context_management": null,
2911 "usage": {
2912 "input_tokens": 100,
2913 "output_tokens": 10,
2914 "cache_creation_input_tokens": 50,
2915 "cache_read_input_tokens": 0,
2916 "service_tier": "standard",
2917 "inference_geo": "not_available"
2918 }
2919 },
2920 "session_id": "abc",
2921 "uuid": "msg-uuid-123"
2922 }"#;
2923
2924 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2925 if let ClaudeOutput::Assistant(asst) = output {
2926 assert_eq!(asst.message.stop_details, None);
2927 assert_eq!(asst.message.context_management, None);
2928 let usage = asst.message.usage.unwrap();
2929 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
2930 } else {
2931 panic!("Expected Assistant message");
2932 }
2933 }
2934
2935 #[test]
2936 fn test_user_message_with_new_fields() {
2937 let json = r#"{
2938 "type": "user",
2939 "message": {
2940 "role": "user",
2941 "content": [{"type": "text", "text": "Hello"}]
2942 },
2943 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2944 "parent_tool_use_id": "toolu_123",
2945 "uuid": "user-msg-456"
2946 }"#;
2947
2948 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2949 if let ClaudeOutput::User(user) = output {
2950 assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
2951 assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
2952 } else {
2953 panic!("Expected User message");
2954 }
2955 }
2956
2957 #[test]
2963 fn test_user_message_preserves_tool_use_result_and_timestamp() {
2964 let json = r#"{
2965 "type":"user",
2966 "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"}]},
2967 "parent_tool_use_id":null,
2968 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
2969 "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
2970 "timestamp":"2026-05-12T23:12:04.121Z",
2971 "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"}}
2972 }"#;
2973
2974 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2975 let user = match output {
2976 ClaudeOutput::User(u) => u,
2977 other => panic!("Expected User message, got {:?}", other.message_type()),
2978 };
2979
2980 assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
2981 let raw = user
2982 .tool_use_result
2983 .as_ref()
2984 .expect("tool_use_result must be captured");
2985 assert_eq!(raw["answers"]["Color"], "Blue");
2986 assert_eq!(raw["questions"][0]["header"], "Color");
2987
2988 let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
2992 assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
2993 assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
2994 assert_eq!(
2995 reser["tool_use_result"]["questions"][0]["question"],
2996 "Which color do you prefer?"
2997 );
2998
2999 let typed: crate::AskUserQuestionInput = user
3002 .tool_use_result_as::<crate::AskUserQuestionInput>()
3003 .expect("tool_use_result present")
3004 .expect("AskUserQuestionInput parses");
3005 assert_eq!(typed.questions.len(), 1);
3006 assert_eq!(typed.questions[0].header, "Color");
3007 let answers = typed.answers.expect("answers populated");
3008 assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3009 }
3010
3011 #[test]
3014 fn test_user_message_without_tool_use_result_omits_field() {
3015 let json = r#"{
3016 "type":"user",
3017 "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3018 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3019 }"#;
3020
3021 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3022 let user = match output {
3023 ClaudeOutput::User(u) => u,
3024 _ => panic!("Expected User message"),
3025 };
3026 assert!(user.tool_use_result.is_none());
3027 assert!(user.timestamp.is_none());
3028
3029 let reser = serde_json::to_value(&user).unwrap();
3030 assert!(reser.get("tool_use_result").is_none());
3031 assert!(reser.get("timestamp").is_none());
3032 }
3033
3034 #[test]
3038 fn test_subagent_result_exposes_token_accounting() {
3039 let json = r#"{
3040 "type":"user",
3041 "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3042 "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3043 "tool_use_result":{
3044 "status":"completed",
3045 "prompt":"Count the .rs files.",
3046 "agentId":"ac4f0276e9d4b6232",
3047 "agentType":"Explore",
3048 "content":[{"type":"text","text":"21"}],
3049 "resolvedModel":"claude-haiku-4-5-20251001",
3050 "totalDurationMs":6869,
3051 "totalTokens":7834,
3052 "totalToolUseCount":1,
3053 "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3054 "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3055 }
3056 }"#;
3057
3058 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3059 let user = match output {
3060 ClaudeOutput::User(u) => u,
3061 _ => panic!("Expected User message"),
3062 };
3063
3064 let result = user.subagent_result().expect("subagent result parses");
3065 assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3066 assert_eq!(
3067 result.resolved_model.as_deref(),
3068 Some("claude-haiku-4-5-20251001")
3069 );
3070 assert_eq!(result.total_tokens, Some(7834));
3071 assert_eq!(result.total_duration_ms, Some(6869));
3072 assert_eq!(result.total_tool_use_count, Some(1));
3073
3074 let usage = result.usage.expect("nested usage present");
3075 assert_eq!(usage.input_tokens, 6);
3076 assert_eq!(usage.cache_read_input_tokens, 7699);
3077
3078 let stats = result.tool_stats.expect("toolStats present");
3079 assert_eq!(stats.bash_count, 1);
3080 }
3081
3082 #[test]
3086 fn test_subagent_result_absent_for_non_task_result() {
3087 let json = r#"{
3088 "type":"user",
3089 "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3090 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3091 "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3092 }"#;
3093
3094 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3095 let user = match output {
3096 ClaudeOutput::User(u) => u,
3097 _ => panic!("Expected User message"),
3098 };
3099
3100 let result = user.subagent_result().expect("lenient parse");
3101 assert_eq!(result.total_tokens, None);
3102 assert_eq!(result.agent_type, None);
3103 }
3104
3105 #[test]
3106 fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3107 use serde_json::Value;
3108
3109 let raw: Value = serde_json::from_str(
3110 r#"{
3111 "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3112 "fast_mode_state":"off",
3113 "fast_mode_disabled_reason":"not_first_party",
3114 "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3115 }"#,
3116 )
3117 .unwrap();
3118 crate::io::assert_fully_wrapped(&raw);
3119
3120 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3121 let ClaudeOutput::System(sys) = output else {
3122 panic!("expected System");
3123 };
3124 let init = sys.as_init().expect("parses as init");
3125 assert_eq!(
3126 init.fast_mode_disabled_reason,
3127 Some(crate::FastModeDisabledReason::NotFirstParty)
3128 );
3129 let errs = init.mcp_server_errors.unwrap();
3130 assert_eq!(errs.len(), 1);
3131 assert_eq!(errs[0].name, "broken");
3132 assert_eq!(errs[0].error_type, "invalid_config");
3133 }
3134
3135 #[test]
3136 fn test_code_change_published_fully_wrapped() {
3137 use super::{KnownSystemEvent, SystemSubtype};
3138 use serde_json::Value;
3139
3140 let raw: Value = serde_json::from_str(
3141 r#"{
3142 "type":"system","subtype":"code_change_published",
3143 "provider":"github","url":"https://github.com/owner/repo/pull/42",
3144 "repo":"owner/repo","identifier":"42",
3145 "uuid":"u1","session_id":"s1"
3146 }"#,
3147 )
3148 .unwrap();
3149 crate::io::assert_fully_wrapped(&raw);
3150
3151 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3152 let ClaudeOutput::System(sys) = output else {
3153 panic!("expected System");
3154 };
3155 assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3156 let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3157 panic!("expected CodeChangePublished event");
3158 };
3159 assert_eq!(msg.provider, "github");
3160 assert_eq!(msg.repo, "owner/repo");
3161 assert_eq!(msg.identifier, "42");
3162
3163 assert!(sys.is_code_change_published());
3164 assert!(!sys.is_vcs_state_changed());
3165 let direct = sys.as_code_change_published().expect("direct accessor");
3166 assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3167 assert!(sys.as_vcs_state_changed().is_none());
3168 }
3169
3170 #[test]
3171 fn test_feedback_draft_queued_fully_wrapped() {
3172 use super::{KnownSystemEvent, SystemSubtype};
3173 use serde_json::Value;
3174
3175 let raw: Value = serde_json::from_str(
3176 r#"{
3177 "type":"system","subtype":"feedback_draft_queued",
3178 "draft_id":"draft-1","draft_type":"bug_report",
3179 "title":"Tool output was truncated",
3180 "details_preview":"The last command omitted its final lines",
3181 "uuid":"u1","session_id":"s1","future_field":"preserved"
3182 }"#,
3183 )
3184 .unwrap();
3185 crate::io::assert_fully_wrapped(&raw);
3186
3187 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3188 let ClaudeOutput::System(sys) = output else {
3189 panic!("expected System");
3190 };
3191 assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3192 assert!(sys.is_feedback_draft_queued());
3193 assert!(!sys.is_vcs_state_changed());
3194
3195 let direct = sys
3196 .as_feedback_draft_queued()
3197 .expect("direct typed accessor");
3198 assert_eq!(direct.draft_id, "draft-1");
3199 assert_eq!(direct.draft_type, "bug_report");
3200 assert_eq!(direct.extra["future_field"], "preserved");
3201
3202 let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3203 panic!("expected FeedbackDraftQueued event");
3204 };
3205 assert_eq!(known.title, "Tool output was truncated");
3206 assert_eq!(
3207 sys.typed_value().expect("typed value")["future_field"],
3208 "preserved"
3209 );
3210 }
3211
3212 #[test]
3213 fn test_vcs_state_changed_fully_wrapped() {
3214 use super::{KnownSystemEvent, VcsMutationKind};
3215 use serde_json::Value;
3216
3217 for kind in ["commit", "push", "merge", "rebase"] {
3218 let raw: Value = serde_json::from_str(&format!(
3219 r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3220 kind
3221 ))
3222 .unwrap();
3223 crate::io::assert_fully_wrapped(&raw);
3224
3225 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3226 let ClaudeOutput::System(sys) = output else {
3227 panic!("expected System");
3228 };
3229 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3230 panic!("expected VcsStateChanged event");
3231 };
3232 assert_eq!(msg.kind.as_str(), kind);
3233 assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3234 }
3235
3236 let raw: Value = serde_json::from_str(
3238 r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3239 )
3240 .unwrap();
3241 crate::io::assert_fully_wrapped(&raw);
3242 let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3243 let ClaudeOutput::System(sys) = output else {
3244 panic!("expected System");
3245 };
3246 let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3247 panic!("expected VcsStateChanged event");
3248 };
3249 assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3250
3251 assert!(sys.is_vcs_state_changed());
3252 let direct = sys.as_vcs_state_changed().expect("direct accessor");
3253 assert_eq!(direct.cwd, "/repo");
3254 assert!(sys.as_code_change_published().is_none());
3255 }
3256
3257 #[test]
3258 fn test_assistant_aborted_and_resume_flags_roundtrip() {
3259 let json = r#"{
3260 "type":"assistant",
3261 "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3262 "session_id":"s1",
3263 "aborted":true,
3264 "resumed_from_incomplete_thinking":true
3265 }"#;
3266 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3267 let ClaudeOutput::Assistant(msg) = &output else {
3268 panic!("expected Assistant");
3269 };
3270 assert_eq!(msg.aborted, Some(true));
3271 assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3272 let reserialized = serde_json::to_string(&output).unwrap();
3273 assert!(reserialized.contains("\"aborted\":true"));
3274 assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3275
3276 let json = r#"{
3278 "type":"assistant",
3279 "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3280 "session_id":"s2"
3281 }"#;
3282 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3283 let reserialized = serde_json::to_string(&output).unwrap();
3284 assert!(!reserialized.contains("aborted"));
3285 assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3286 }
3287
3288 #[test]
3289 fn test_user_tool_result_meta_roundtrip() {
3290 let json = r#"{
3291 "type":"user",
3292 "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3293 "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3294 "tool_result_meta":[
3295 {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3296 {"id":"toolu_2","non_execution_kind":"permission-rule"}
3297 ]
3298 }"#;
3299 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3300 let ClaudeOutput::User(user) = &output else {
3301 panic!("expected User");
3302 };
3303 let meta = user.tool_result_meta.as_ref().unwrap();
3304 assert_eq!(meta.len(), 2);
3305 assert_eq!(meta[0].non_execution_kind, "user-rejected");
3306 assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3307 assert_eq!(meta[1].user_feedback, None);
3308
3309 let reserialized = serde_json::to_string(&output).unwrap();
3310 assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3311 assert!(!reserialized.contains("\"user_feedback\":null"));
3312 }
3313
3314 #[test]
3318 fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3319 use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3320 let with_scope = serde_json::json!({
3321 "trigger": "refusal",
3322 "direction": "retry",
3323 "scope": "local",
3324 "original_model": "claude-fable-5",
3325 "fallback_model": "claude-opus-5",
3326 "request_id": null,
3327 "content": "Refused; retried on fallback model.",
3328 "uuid": "u1",
3329 "session_id": "s1"
3330 });
3331 let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3332 assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3333 assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3334
3335 let mut without = with_scope.clone();
3338 without.as_object_mut().unwrap().remove("scope");
3339 let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3340 assert_eq!(msg.scope, None);
3341 assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3342
3343 assert_eq!(
3345 RefusalFallbackScope::from("workspace").as_str(),
3346 "workspace"
3347 );
3348 }
3349}