Skip to main content

claude_codes/io/
message_types.rs

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};
8use super::rate_limit::RateLimitInfo;
9
10/// Known system message subtypes.
11///
12/// The Claude CLI emits system messages with a `subtype` field indicating what
13/// kind of system event occurred. This enum captures the known subtypes while
14/// preserving unknown values via the `Unknown` variant for forward compatibility.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum SystemSubtype {
17    Init,
18    Status,
19    CompactBoundary,
20    ThinkingTokens,
21    TaskStarted,
22    TaskProgress,
23    TaskUpdated,
24    TaskNotification,
25    ApiRetry,
26    ControlRequestProgress,
27    ModelRefusalFallback,
28    ModelRefusalNoFallback,
29    LocalCommandOutput,
30    HookStarted,
31    HookProgress,
32    HookResponse,
33    PluginInstall,
34    BackgroundTasksChanged,
35    SessionStateChanged,
36    WorkerShuttingDown,
37    CommandsChanged,
38    Notification,
39    FilesPersisted,
40    MemoryRecall,
41    ElicitationComplete,
42    PermissionDenied,
43    MirrorError,
44    Informational,
45    CodeChangePublished,
46    VcsStateChanged,
47    FeedbackDraftQueued,
48    CloudSessionDelta,
49    DevIntent,
50    TurnHandoffAvailable,
51    TurnPreempted,
52    PeerMessageHold,
53    PerTurnEffortChanged,
54    SessionTitleChanged,
55    SessionWritesLost,
56    InstructionSizeWarning,
57    FileAttachmentsMissing,
58    PermissionCheckStatus,
59    InheritedAsksSettled,
60    UiFocus,
61    UiInvalidate,
62    UiLog,
63    UiNotify,
64    UiNotifyCancel,
65    UiPanes,
66    UiPlaced,
67    UiScroll,
68    UiStatus,
69    UiToast,
70    /// A subtype not yet known to this version of the crate.
71    Unknown(String),
72}
73
74impl SystemSubtype {
75    pub fn as_str(&self) -> &str {
76        match self {
77            Self::Init => "init",
78            Self::Status => "status",
79            Self::CompactBoundary => "compact_boundary",
80            Self::ThinkingTokens => "thinking_tokens",
81            Self::TaskStarted => "task_started",
82            Self::TaskProgress => "task_progress",
83            Self::TaskUpdated => "task_updated",
84            Self::TaskNotification => "task_notification",
85            Self::ApiRetry => "api_retry",
86            Self::ControlRequestProgress => "control_request_progress",
87            Self::ModelRefusalFallback => "model_refusal_fallback",
88            Self::ModelRefusalNoFallback => "model_refusal_no_fallback",
89            Self::LocalCommandOutput => "local_command_output",
90            Self::HookStarted => "hook_started",
91            Self::HookProgress => "hook_progress",
92            Self::HookResponse => "hook_response",
93            Self::PluginInstall => "plugin_install",
94            Self::BackgroundTasksChanged => "background_tasks_changed",
95            Self::SessionStateChanged => "session_state_changed",
96            Self::WorkerShuttingDown => "worker_shutting_down",
97            Self::CommandsChanged => "commands_changed",
98            Self::Notification => "notification",
99            Self::FilesPersisted => "files_persisted",
100            Self::MemoryRecall => "memory_recall",
101            Self::ElicitationComplete => "elicitation_complete",
102            Self::PermissionDenied => "permission_denied",
103            Self::MirrorError => "mirror_error",
104            Self::Informational => "informational",
105            Self::CodeChangePublished => "code_change_published",
106            Self::VcsStateChanged => "vcs_state_changed",
107            Self::FeedbackDraftQueued => "feedback_draft_queued",
108            Self::CloudSessionDelta => "cloud_session_delta",
109            Self::DevIntent => "dev_intent",
110            Self::TurnHandoffAvailable => "turn_handoff_available",
111            Self::TurnPreempted => "turn_preempted",
112            Self::PeerMessageHold => "peer_message_hold",
113            Self::PerTurnEffortChanged => "per_turn_effort_changed",
114            Self::SessionTitleChanged => "session_title_changed",
115            Self::SessionWritesLost => "session_writes_lost",
116            Self::InstructionSizeWarning => "instruction_size_warning",
117            Self::FileAttachmentsMissing => "file_attachments_missing",
118            Self::PermissionCheckStatus => "permission_check_status",
119            Self::InheritedAsksSettled => "inherited_asks_settled",
120            Self::UiFocus => "ui_focus",
121            Self::UiInvalidate => "ui_invalidate",
122            Self::UiLog => "ui_log",
123            Self::UiNotify => "ui_notify",
124            Self::UiNotifyCancel => "ui_notify_cancel",
125            Self::UiPanes => "ui_panes",
126            Self::UiPlaced => "ui_placed",
127            Self::UiScroll => "ui_scroll",
128            Self::UiStatus => "ui_status",
129            Self::UiToast => "ui_toast",
130            Self::Unknown(s) => s.as_str(),
131        }
132    }
133}
134
135impl fmt::Display for SystemSubtype {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(self.as_str())
138    }
139}
140
141impl From<&str> for SystemSubtype {
142    fn from(s: &str) -> Self {
143        match s {
144            "init" => Self::Init,
145            "status" => Self::Status,
146            "compact_boundary" => Self::CompactBoundary,
147            "thinking_tokens" => Self::ThinkingTokens,
148            "task_started" => Self::TaskStarted,
149            "task_progress" => Self::TaskProgress,
150            "task_updated" => Self::TaskUpdated,
151            "task_notification" => Self::TaskNotification,
152            "api_retry" => Self::ApiRetry,
153            "control_request_progress" => Self::ControlRequestProgress,
154            "model_refusal_fallback" => Self::ModelRefusalFallback,
155            "model_refusal_no_fallback" => Self::ModelRefusalNoFallback,
156            "local_command_output" => Self::LocalCommandOutput,
157            "hook_started" => Self::HookStarted,
158            "hook_progress" => Self::HookProgress,
159            "hook_response" => Self::HookResponse,
160            "plugin_install" => Self::PluginInstall,
161            "background_tasks_changed" => Self::BackgroundTasksChanged,
162            "session_state_changed" => Self::SessionStateChanged,
163            "worker_shutting_down" => Self::WorkerShuttingDown,
164            "commands_changed" => Self::CommandsChanged,
165            "notification" => Self::Notification,
166            "files_persisted" => Self::FilesPersisted,
167            "memory_recall" => Self::MemoryRecall,
168            "elicitation_complete" => Self::ElicitationComplete,
169            "permission_denied" => Self::PermissionDenied,
170            "mirror_error" => Self::MirrorError,
171            "informational" => Self::Informational,
172            "code_change_published" => Self::CodeChangePublished,
173            "vcs_state_changed" => Self::VcsStateChanged,
174            "feedback_draft_queued" => Self::FeedbackDraftQueued,
175            "cloud_session_delta" => Self::CloudSessionDelta,
176            "dev_intent" => Self::DevIntent,
177            "turn_handoff_available" => Self::TurnHandoffAvailable,
178            "turn_preempted" => Self::TurnPreempted,
179            "peer_message_hold" => Self::PeerMessageHold,
180            "per_turn_effort_changed" => Self::PerTurnEffortChanged,
181            "session_title_changed" => Self::SessionTitleChanged,
182            "session_writes_lost" => Self::SessionWritesLost,
183            "instruction_size_warning" => Self::InstructionSizeWarning,
184            "file_attachments_missing" => Self::FileAttachmentsMissing,
185            "permission_check_status" => Self::PermissionCheckStatus,
186            "inherited_asks_settled" => Self::InheritedAsksSettled,
187            "ui_focus" => Self::UiFocus,
188            "ui_invalidate" => Self::UiInvalidate,
189            "ui_log" => Self::UiLog,
190            "ui_notify" => Self::UiNotify,
191            "ui_notify_cancel" => Self::UiNotifyCancel,
192            "ui_panes" => Self::UiPanes,
193            "ui_placed" => Self::UiPlaced,
194            "ui_scroll" => Self::UiScroll,
195            "ui_status" => Self::UiStatus,
196            "ui_toast" => Self::UiToast,
197            other => Self::Unknown(other.to_string()),
198        }
199    }
200}
201
202impl Serialize for SystemSubtype {
203    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
204        serializer.serialize_str(self.as_str())
205    }
206}
207
208impl<'de> Deserialize<'de> for SystemSubtype {
209    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210        let s = String::deserialize(deserializer)?;
211        Ok(Self::from(s.as_str()))
212    }
213}
214
215/// Known message roles.
216///
217/// Used in `MessageContent` and `AssistantMessageContent` to indicate the
218/// speaker of a message.
219#[derive(Debug, Clone, PartialEq, Eq, Hash)]
220pub enum MessageRole {
221    User,
222    Assistant,
223    /// A role not yet known to this version of the crate.
224    Unknown(String),
225}
226
227impl MessageRole {
228    pub fn as_str(&self) -> &str {
229        match self {
230            Self::User => "user",
231            Self::Assistant => "assistant",
232            Self::Unknown(s) => s.as_str(),
233        }
234    }
235}
236
237impl fmt::Display for MessageRole {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.write_str(self.as_str())
240    }
241}
242
243impl From<&str> for MessageRole {
244    fn from(s: &str) -> Self {
245        match s {
246            "user" => Self::User,
247            "assistant" => Self::Assistant,
248            other => Self::Unknown(other.to_string()),
249        }
250    }
251}
252
253impl Serialize for MessageRole {
254    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
255        serializer.serialize_str(self.as_str())
256    }
257}
258
259impl<'de> Deserialize<'de> for MessageRole {
260    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
261        let s = String::deserialize(deserializer)?;
262        Ok(Self::from(s.as_str()))
263    }
264}
265
266/// What triggered a context compaction.
267#[derive(Debug, Clone, PartialEq, Eq, Hash)]
268pub enum CompactionTrigger {
269    /// Automatic compaction triggered by token limit.
270    Auto,
271    /// User-initiated compaction (e.g., /compact command).
272    Manual,
273    /// A trigger not yet known to this version of the crate.
274    Unknown(String),
275}
276
277impl CompactionTrigger {
278    pub fn as_str(&self) -> &str {
279        match self {
280            Self::Auto => "auto",
281            Self::Manual => "manual",
282            Self::Unknown(s) => s.as_str(),
283        }
284    }
285}
286
287impl fmt::Display for CompactionTrigger {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        f.write_str(self.as_str())
290    }
291}
292
293impl From<&str> for CompactionTrigger {
294    fn from(s: &str) -> Self {
295        match s {
296            "auto" => Self::Auto,
297            "manual" => Self::Manual,
298            other => Self::Unknown(other.to_string()),
299        }
300    }
301}
302
303impl Serialize for CompactionTrigger {
304    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
305        serializer.serialize_str(self.as_str())
306    }
307}
308
309impl<'de> Deserialize<'de> for CompactionTrigger {
310    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
311        let s = String::deserialize(deserializer)?;
312        Ok(Self::from(s.as_str()))
313    }
314}
315
316/// Reason why the assistant stopped generating.
317#[derive(Debug, Clone, PartialEq, Eq, Hash)]
318pub enum StopReason {
319    /// The assistant reached a natural end of its turn.
320    EndTurn,
321    /// The response hit the maximum token limit.
322    MaxTokens,
323    /// The assistant wants to use a tool.
324    ToolUse,
325    /// A stop reason not yet known to this version of the crate.
326    Unknown(String),
327}
328
329impl StopReason {
330    pub fn as_str(&self) -> &str {
331        match self {
332            Self::EndTurn => "end_turn",
333            Self::MaxTokens => "max_tokens",
334            Self::ToolUse => "tool_use",
335            Self::Unknown(s) => s.as_str(),
336        }
337    }
338}
339
340impl fmt::Display for StopReason {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        f.write_str(self.as_str())
343    }
344}
345
346impl From<&str> for StopReason {
347    fn from(s: &str) -> Self {
348        match s {
349            "end_turn" => Self::EndTurn,
350            "max_tokens" => Self::MaxTokens,
351            "tool_use" => Self::ToolUse,
352            other => Self::Unknown(other.to_string()),
353        }
354    }
355}
356
357impl Serialize for StopReason {
358    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
359        serializer.serialize_str(self.as_str())
360    }
361}
362
363impl<'de> Deserialize<'de> for StopReason {
364    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
365        let s = String::deserialize(deserializer)?;
366        Ok(Self::from(s.as_str()))
367    }
368}
369
370/// How the API key was sourced for the session.
371#[derive(Debug, Clone, PartialEq, Eq, Hash)]
372pub enum ApiKeySource {
373    /// No API key provided.
374    None,
375    User,
376    Project,
377    Org,
378    Temporary,
379    Oauth,
380    /// A source not yet known to this version of the crate.
381    Unknown(String),
382}
383
384impl ApiKeySource {
385    pub fn as_str(&self) -> &str {
386        match self {
387            Self::None => "none",
388            Self::User => "user",
389            Self::Project => "project",
390            Self::Org => "org",
391            Self::Temporary => "temporary",
392            Self::Oauth => "oauth",
393            Self::Unknown(s) => s.as_str(),
394        }
395    }
396}
397
398impl fmt::Display for ApiKeySource {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        f.write_str(self.as_str())
401    }
402}
403
404impl From<&str> for ApiKeySource {
405    fn from(s: &str) -> Self {
406        match s {
407            "none" => Self::None,
408            "user" => Self::User,
409            "project" => Self::Project,
410            "org" => Self::Org,
411            "temporary" => Self::Temporary,
412            "oauth" => Self::Oauth,
413            other => Self::Unknown(other.to_string()),
414        }
415    }
416}
417
418impl Serialize for ApiKeySource {
419    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
420        serializer.serialize_str(self.as_str())
421    }
422}
423
424impl<'de> Deserialize<'de> for ApiKeySource {
425    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
426        let s = String::deserialize(deserializer)?;
427        Ok(Self::from(s.as_str()))
428    }
429}
430
431/// Output formatting style for the session.
432#[derive(Debug, Clone, PartialEq, Eq, Hash)]
433pub enum OutputStyle {
434    /// Default output style.
435    Default,
436    /// A style not yet known to this version of the crate.
437    Unknown(String),
438}
439
440impl OutputStyle {
441    pub fn as_str(&self) -> &str {
442        match self {
443            Self::Default => "default",
444            Self::Unknown(s) => s.as_str(),
445        }
446    }
447}
448
449impl fmt::Display for OutputStyle {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        f.write_str(self.as_str())
452    }
453}
454
455impl From<&str> for OutputStyle {
456    fn from(s: &str) -> Self {
457        match s {
458            "default" => Self::Default,
459            other => Self::Unknown(other.to_string()),
460        }
461    }
462}
463
464impl Serialize for OutputStyle {
465    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
466        serializer.serialize_str(self.as_str())
467    }
468}
469
470impl<'de> Deserialize<'de> for OutputStyle {
471    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
472        let s = String::deserialize(deserializer)?;
473        Ok(Self::from(s.as_str()))
474    }
475}
476
477/// Permission mode reported in init messages.
478#[derive(Debug, Clone, PartialEq, Eq, Hash)]
479pub enum InitPermissionMode {
480    /// Default permission mode.
481    Default,
482    AcceptEdits,
483    BypassPermissions,
484    Plan,
485    DontAsk,
486    Auto,
487    /// A mode not yet known to this version of the crate.
488    Unknown(String),
489}
490
491impl InitPermissionMode {
492    pub fn as_str(&self) -> &str {
493        match self {
494            Self::Default => "default",
495            Self::AcceptEdits => "acceptEdits",
496            Self::BypassPermissions => "bypassPermissions",
497            Self::Plan => "plan",
498            Self::DontAsk => "dontAsk",
499            Self::Auto => "auto",
500            Self::Unknown(s) => s.as_str(),
501        }
502    }
503}
504
505impl fmt::Display for InitPermissionMode {
506    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507        f.write_str(self.as_str())
508    }
509}
510
511impl From<&str> for InitPermissionMode {
512    fn from(s: &str) -> Self {
513        match s {
514            "default" => Self::Default,
515            "acceptEdits" => Self::AcceptEdits,
516            "bypassPermissions" => Self::BypassPermissions,
517            "plan" => Self::Plan,
518            "dontAsk" => Self::DontAsk,
519            "auto" => Self::Auto,
520            other => Self::Unknown(other.to_string()),
521        }
522    }
523}
524
525impl Serialize for InitPermissionMode {
526    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
527        serializer.serialize_str(self.as_str())
528    }
529}
530
531impl<'de> Deserialize<'de> for InitPermissionMode {
532    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
533        let s = String::deserialize(deserializer)?;
534        Ok(Self::from(s.as_str()))
535    }
536}
537
538/// Status of an ongoing operation (e.g., context compaction).
539#[derive(Debug, Clone, PartialEq, Eq, Hash)]
540pub enum StatusMessageStatus {
541    /// Context compaction is in progress.
542    Compacting,
543    /// The CLI is issuing a request.
544    Requesting,
545    /// A status not yet known to this version of the crate.
546    Unknown(String),
547}
548
549impl StatusMessageStatus {
550    pub fn as_str(&self) -> &str {
551        match self {
552            Self::Compacting => "compacting",
553            Self::Requesting => "requesting",
554            Self::Unknown(s) => s.as_str(),
555        }
556    }
557}
558
559impl fmt::Display for StatusMessageStatus {
560    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561        f.write_str(self.as_str())
562    }
563}
564
565impl From<&str> for StatusMessageStatus {
566    fn from(s: &str) -> Self {
567        match s {
568            "compacting" => Self::Compacting,
569            "requesting" => Self::Requesting,
570            other => Self::Unknown(other.to_string()),
571        }
572    }
573}
574
575impl Serialize for StatusMessageStatus {
576    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
577        serializer.serialize_str(self.as_str())
578    }
579}
580
581impl<'de> Deserialize<'de> for StatusMessageStatus {
582    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
583        let s = String::deserialize(deserializer)?;
584        Ok(Self::from(s.as_str()))
585    }
586}
587
588/// Serialize an optional UUID as a string
589pub(crate) fn serialize_optional_uuid<S>(
590    uuid: &Option<Uuid>,
591    serializer: S,
592) -> Result<S::Ok, S::Error>
593where
594    S: Serializer,
595{
596    match uuid {
597        Some(id) => serializer.serialize_str(&id.to_string()),
598        None => serializer.serialize_none(),
599    }
600}
601
602/// Deserialize an optional UUID from a string
603pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
604where
605    D: Deserializer<'de>,
606{
607    let opt_str: Option<String> = Option::deserialize(deserializer)?;
608    match opt_str {
609        Some(s) => Uuid::parse_str(&s)
610            .map(Some)
611            .map_err(serde::de::Error::custom),
612        None => Ok(None),
613    }
614}
615
616/// Message provenance. The `kind` field is the stable discriminator; variant
617/// specific fields are preserved in `extra` for forward-compatible access.
618#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
619pub struct MessageOrigin {
620    pub kind: String,
621    #[serde(flatten)]
622    pub extra: serde_json::Map<String, Value>,
623}
624
625/// Metadata attached when user-visible transcript content summarizes prior messages.
626#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
627pub struct SummarizeMetadata {
628    pub messages_summarized: u64,
629    #[serde(default, skip_serializing_if = "Option::is_none")]
630    pub user_context: Option<String>,
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub direction: Option<String>,
633}
634
635/// MCP metadata passed through on user-message wrappers.
636#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
637pub struct McpMeta {
638    #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
639    pub meta: Option<Value>,
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub structured_content: Option<Value>,
642    /// The `resource_link` content blocks the MCP tool returned, collected
643    /// from the raw result before the CLI rewrites each into the
644    /// `[Resource link: NAME] URI` text line the model reads. At most 50
645    /// links and 64 KiB serialized; absent when the result had none.
646    #[serde(default, skip_serializing_if = "Vec::is_empty")]
647    pub resource_links: Vec<ResourceLink>,
648}
649
650/// A file an MCP tool returned by reference — a `resource_link` content block
651/// as carried on [`McpMeta::resource_links`] and
652/// [`TaskNotificationMessage::resource_links`] (CLI 2.1.259+).
653#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
654pub struct ResourceLink {
655    pub uri: String,
656    pub name: String,
657    #[serde(default, skip_serializing_if = "Option::is_none")]
658    pub title: Option<String>,
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub description: Option<String>,
661    #[serde(default, skip_serializing_if = "Option::is_none", rename = "mimeType")]
662    pub mime_type: Option<String>,
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    pub size: Option<u64>,
665    #[serde(default, skip_serializing_if = "Option::is_none")]
666    pub annotations: Option<Value>,
667}
668
669/// Display metadata for a `tool_result` block carried on the user wrapper.
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
671pub struct ToolResultMeta {
672    /// The `tool_use_id` of the matching `tool_result` block.
673    pub id: String,
674    /// Harness-stamped reason an `is_error: true` result did not carry the
675    /// tool's own execution output (`user-rejected`, `permission-rule`,
676    /// `automode-*`, `interrupted`, `cancelled`). Open set — treat
677    /// unrecognized values as valid reasons; absent means the tool ran to
678    /// completion. Optional since CLI 2.1.276, when an entry may carry a
679    /// [`remedy`](Self::remedy) alone.
680    #[serde(default, skip_serializing_if = "Option::is_none")]
681    pub non_execution_kind: Option<String>,
682    /// The deny comment a human typed at a permission prompt, when present.
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub user_feedback: Option<String>,
685    /// The fix a host can offer for what the result reports (CLI 2.1.276+).
686    /// May be present on a result that did run (a staged settings write, a
687    /// sandbox violation on exit 0), so read whether the call ran from
688    /// [`non_execution_kind`](Self::non_execution_kind), not from this.
689    #[serde(default, skip_serializing_if = "Option::is_none")]
690    pub remedy: Option<ToolResultRemedy>,
691    /// How the call's permission check ended (CLI 2.1.293+). On a
692    /// subagent's messages (`parent_tool_use_id` set) entries carry only
693    /// this, so those messages do not say whether a call ran. Absent when
694    /// the call ended before its permission check, and on some results
695    /// built apart from the call's own run (a plugin hook's refusal, a
696    /// hook's rewrite of a denied result, a queued call a stop cancelled).
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub permission_decision: Option<ToolPermissionDecision>,
699    /// The name of the Claude Code built-in tool whose own completed run, in
700    /// the process that wrote this message, made the result. Stamped for
701    /// the few built-ins a host acts on; absent once a hook rewrote the
702    /// result, on a message read from a session file or taken from another
703    /// frame (a resumed session), on a result a client wrote (a handed-over
704    /// turn), and on a subagent's messages (CLI 2.1.317+).
705    #[serde(default, skip_serializing_if = "Option::is_none")]
706    pub built_in_tool: Option<String>,
707}
708
709/// [`ToolResultMeta::permission_decision`] — how a tool call's permission
710/// check ended, as Claude Code's `claude_code.tool.blocked_on_user` trace
711/// span reports it (CLI 2.1.293+).
712#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
713pub struct ToolPermissionDecision {
714    /// The outcome.
715    pub decision: PermissionDecisionOutcome,
716    /// What decided: `config`, `classifier`, `hook`, `user_temporary`,
717    /// `user_permanent`, `user_reject`, `user_abort`, or `invalid_input`,
718    /// `permission_updated_input_invalid` or `server_fallback_tombstone`
719    /// when bad input or the server ended the wait. In SDK sessions a rule
720    /// for this session or in the user's own settings reports
721    /// `user_temporary`, `user_permanent` or `user_reject` and other rules
722    /// report `config`; interactive sessions report every rule as `config`.
723    pub source: String,
724    /// The kind of reason the check gave when it decided by itself, such as
725    /// `rule` whenever a rule decided, or `classifier` for an auto-mode
726    /// allow (which reports source `config`). Absent when a person or the
727    /// SDK host answered, when the outcome was still a question, and for
728    /// `user_abort` and the last three sources above.
729    #[serde(default, skip_serializing_if = "Option::is_none")]
730    pub reason_type: Option<PermissionDecisionReasonType>,
731    /// Beside a `classifier` [`reason_type`](Self::reason_type): the
732    /// server-side auto-mode classifier named an entry in the session's
733    /// auto-mode settings — an `allow` exception for an accept, a
734    /// `soft_deny` entry for a reject — and this is that entry
735    /// (CLI 2.1.319+).
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub auto_mode_rule: Option<AutoModeRule>,
738}
739
740/// [`ToolPermissionDecision::auto_mode_rule`] — the auto-mode settings entry
741/// the classifier named (CLI 2.1.319+).
742#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
743pub struct AutoModeRule {
744    /// Which list the entry is on.
745    pub list: AutoModeRuleList,
746    /// The entry's name.
747    pub name: String,
748}
749
750/// [`AutoModeRule::list`].
751#[derive(Debug, Clone, PartialEq, Eq, Hash)]
752pub enum AutoModeRuleList {
753    /// An `allow` exception.
754    Allow,
755    /// A `soft_deny` entry.
756    SoftDeny,
757    /// A list not yet known to this version of the crate.
758    Unknown(String),
759}
760
761impl AutoModeRuleList {
762    pub fn as_str(&self) -> &str {
763        match self {
764            Self::Allow => "allow",
765            Self::SoftDeny => "soft_deny",
766            Self::Unknown(s) => s.as_str(),
767        }
768    }
769}
770
771impl fmt::Display for AutoModeRuleList {
772    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773        f.write_str(self.as_str())
774    }
775}
776
777impl From<&str> for AutoModeRuleList {
778    fn from(s: &str) -> Self {
779        match s {
780            "allow" => Self::Allow,
781            "soft_deny" => Self::SoftDeny,
782            other => Self::Unknown(other.to_string()),
783        }
784    }
785}
786
787impl Serialize for AutoModeRuleList {
788    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
789        serializer.serialize_str(self.as_str())
790    }
791}
792
793impl<'de> Deserialize<'de> for AutoModeRuleList {
794    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
795        let s = String::deserialize(deserializer)?;
796        Ok(Self::from(s.as_str()))
797    }
798}
799
800/// [`ToolPermissionDecision::decision`].
801#[derive(Debug, Clone, PartialEq, Eq, Hash)]
802pub enum PermissionDecisionOutcome {
803    /// The call was allowed.
804    Accept,
805    /// The call was denied.
806    Reject,
807    /// The permission wait was cancelled.
808    Cancelled,
809    /// An outcome not yet known to this version of the crate.
810    Unknown(String),
811}
812
813impl PermissionDecisionOutcome {
814    pub fn as_str(&self) -> &str {
815        match self {
816            Self::Accept => "accept",
817            Self::Reject => "reject",
818            Self::Cancelled => "cancelled",
819            Self::Unknown(s) => s.as_str(),
820        }
821    }
822}
823
824impl fmt::Display for PermissionDecisionOutcome {
825    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
826        f.write_str(self.as_str())
827    }
828}
829
830impl From<&str> for PermissionDecisionOutcome {
831    fn from(s: &str) -> Self {
832        match s {
833            "accept" => Self::Accept,
834            "reject" => Self::Reject,
835            "cancelled" => Self::Cancelled,
836            other => Self::Unknown(other.to_string()),
837        }
838    }
839}
840
841impl Serialize for PermissionDecisionOutcome {
842    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
843        serializer.serialize_str(self.as_str())
844    }
845}
846
847impl<'de> Deserialize<'de> for PermissionDecisionOutcome {
848    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
849        let s = String::deserialize(deserializer)?;
850        Ok(Self::from(s.as_str()))
851    }
852}
853
854/// [`ToolPermissionDecision::reason_type`] — the kind of reason a
855/// self-decided permission check gave.
856#[derive(Debug, Clone, PartialEq, Eq, Hash)]
857pub enum PermissionDecisionReasonType {
858    /// A permission rule decided.
859    Rule,
860    /// The permission mode decided.
861    Mode,
862    /// The results of a compound command's subcommands decided.
863    SubcommandResults,
864    /// The `--permission-prompt-tool` decided.
865    PermissionPromptTool,
866    /// A hook decided.
867    Hook,
868    /// An async agent's permission handling decided.
869    AsyncAgent,
870    /// A sandbox override decided.
871    SandboxOverride,
872    /// The working-directory check decided.
873    WorkingDir,
874    /// A safety check decided.
875    SafetyCheck,
876    /// The auto-mode classifier decided.
877    Classifier,
878    /// Some other reason.
879    Other,
880    /// A reason type not yet known to this version of the crate.
881    Unknown(String),
882}
883
884impl PermissionDecisionReasonType {
885    pub fn as_str(&self) -> &str {
886        match self {
887            Self::Rule => "rule",
888            Self::Mode => "mode",
889            Self::SubcommandResults => "subcommandResults",
890            Self::PermissionPromptTool => "permissionPromptTool",
891            Self::Hook => "hook",
892            Self::AsyncAgent => "asyncAgent",
893            Self::SandboxOverride => "sandboxOverride",
894            Self::WorkingDir => "workingDir",
895            Self::SafetyCheck => "safetyCheck",
896            Self::Classifier => "classifier",
897            Self::Other => "other",
898            Self::Unknown(s) => s.as_str(),
899        }
900    }
901}
902
903impl fmt::Display for PermissionDecisionReasonType {
904    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
905        f.write_str(self.as_str())
906    }
907}
908
909impl From<&str> for PermissionDecisionReasonType {
910    fn from(s: &str) -> Self {
911        match s {
912            "rule" => Self::Rule,
913            "mode" => Self::Mode,
914            "subcommandResults" => Self::SubcommandResults,
915            "permissionPromptTool" => Self::PermissionPromptTool,
916            "hook" => Self::Hook,
917            "asyncAgent" => Self::AsyncAgent,
918            "sandboxOverride" => Self::SandboxOverride,
919            "workingDir" => Self::WorkingDir,
920            "safetyCheck" => Self::SafetyCheck,
921            "classifier" => Self::Classifier,
922            "other" => Self::Other,
923            other => Self::Unknown(other.to_string()),
924        }
925    }
926}
927
928impl Serialize for PermissionDecisionReasonType {
929    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
930        serializer.serialize_str(self.as_str())
931    }
932}
933
934impl<'de> Deserialize<'de> for PermissionDecisionReasonType {
935    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
936        let s = String::deserialize(deserializer)?;
937        Ok(Self::from(s.as_str()))
938    }
939}
940
941/// The fix a host can offer for what a `tool_result` reports, stamped from
942/// structured producer state (never parsed from the result text), carried
943/// as [`ToolResultMeta::remedy`] (CLI 2.1.276+). Only the parameters the
944/// [`kind`](Self::kind) needs are present.
945#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
946pub struct ToolResultRemedy {
947    /// What the host can fix. New kinds are additive — treat
948    /// [`ToolResultRemedyKind::Unknown`] as no remedy.
949    pub kind: ToolResultRemedyKind,
950    /// `mcp_needs_auth` / `mcp_disabled`: the server names as configured.
951    /// `mcp_required_missing`: the name patterns the agent definition
952    /// requires that no connected server matches.
953    #[serde(default, skip_serializing_if = "Option::is_none")]
954    pub servers: Option<Vec<String>>,
955    /// `auth_expired` / `auth_overridden` / `auth_missing_scope` /
956    /// `design_needs_authorization`: whose login the fix names.
957    #[serde(default, skip_serializing_if = "Option::is_none")]
958    pub provider: Option<RemedyLoginProvider>,
959    /// `outside_reads_blocked`: the path the read block refused, when the
960    /// refusal names one. `staged_for_review`: the settings file the write
961    /// was held for.
962    #[serde(default, skip_serializing_if = "Option::is_none")]
963    pub path: Option<String>,
964    /// `outside_reads_blocked`: an organization policy sets the block, so
965    /// the user cannot remove it. `auth_overridden`: the overriding
966    /// credential was injected by the host environment, so nothing in this
967    /// session can unset it.
968    #[serde(default, skip_serializing_if = "Option::is_none")]
969    pub managed: Option<bool>,
970    /// `feature_disabled` / `policy_denied`: the feature that is off.
971    #[serde(default, skip_serializing_if = "Option::is_none")]
972    pub feature: Option<RemedyFeature>,
973    /// `feature_disabled`: why the feature is off.
974    #[serde(default, skip_serializing_if = "Option::is_none")]
975    pub cause: Option<RemedyFeatureCause>,
976    /// `policy_denied`: the verdict.
977    #[serde(default, skip_serializing_if = "Option::is_none")]
978    pub policy_kind: Option<RemedyPolicyKind>,
979}
980
981/// What a host can fix for a tool result ([`ToolResultRemedy::kind`]).
982#[derive(Debug, Clone, PartialEq, Eq, Hash)]
983pub enum ToolResultRemedyKind {
984    /// An MCP server needs its login completed.
985    McpNeedsAuth,
986    /// An MCP server is disabled in settings.
987    McpDisabled,
988    /// The agent definition requires MCP servers that are not connected.
989    McpRequiredMissing,
990    /// The named login has expired.
991    AuthExpired,
992    /// A host-injected credential overrides the named login.
993    AuthOverridden,
994    /// The named login lacks a scope the call needed.
995    AuthMissingScope,
996    /// Claude Design needs its own authorization.
997    DesignNeedsAuthorization,
998    /// The command violated the sandbox policy.
999    SandboxViolation,
1000    /// A read outside the working directories was refused.
1001    OutsideReadsBlocked,
1002    /// Memory is paused by `/pause-memory`.
1003    MemoryPaused,
1004    /// A feature is switched off; see [`ToolResultRemedy::cause`].
1005    FeatureDisabled,
1006    /// An organization policy denies a feature; see
1007    /// [`ToolResultRemedy::policy_kind`].
1008    PolicyDenied,
1009    /// The command's exec image exceeded the OS argument limit.
1010    SpawnArgLimit,
1011    /// A settings-file write was held for the machine owner's review.
1012    StagedForReview,
1013    /// A remedy kind not yet known to this version of the crate.
1014    Unknown(String),
1015}
1016
1017impl ToolResultRemedyKind {
1018    pub fn as_str(&self) -> &str {
1019        match self {
1020            Self::McpNeedsAuth => "mcp_needs_auth",
1021            Self::McpDisabled => "mcp_disabled",
1022            Self::McpRequiredMissing => "mcp_required_missing",
1023            Self::AuthExpired => "auth_expired",
1024            Self::AuthOverridden => "auth_overridden",
1025            Self::AuthMissingScope => "auth_missing_scope",
1026            Self::DesignNeedsAuthorization => "design_needs_authorization",
1027            Self::SandboxViolation => "sandbox_violation",
1028            Self::OutsideReadsBlocked => "outside_reads_blocked",
1029            Self::MemoryPaused => "memory_paused",
1030            Self::FeatureDisabled => "feature_disabled",
1031            Self::PolicyDenied => "policy_denied",
1032            Self::SpawnArgLimit => "spawn_arg_limit",
1033            Self::StagedForReview => "staged_for_review",
1034            Self::Unknown(s) => s.as_str(),
1035        }
1036    }
1037}
1038
1039impl fmt::Display for ToolResultRemedyKind {
1040    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1041        f.write_str(self.as_str())
1042    }
1043}
1044
1045impl From<&str> for ToolResultRemedyKind {
1046    fn from(s: &str) -> Self {
1047        match s {
1048            "mcp_needs_auth" => Self::McpNeedsAuth,
1049            "mcp_disabled" => Self::McpDisabled,
1050            "mcp_required_missing" => Self::McpRequiredMissing,
1051            "auth_expired" => Self::AuthExpired,
1052            "auth_overridden" => Self::AuthOverridden,
1053            "auth_missing_scope" => Self::AuthMissingScope,
1054            "design_needs_authorization" => Self::DesignNeedsAuthorization,
1055            "sandbox_violation" => Self::SandboxViolation,
1056            "outside_reads_blocked" => Self::OutsideReadsBlocked,
1057            "memory_paused" => Self::MemoryPaused,
1058            "feature_disabled" => Self::FeatureDisabled,
1059            "policy_denied" => Self::PolicyDenied,
1060            "spawn_arg_limit" => Self::SpawnArgLimit,
1061            "staged_for_review" => Self::StagedForReview,
1062            other => Self::Unknown(other.to_string()),
1063        }
1064    }
1065}
1066
1067impl Serialize for ToolResultRemedyKind {
1068    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1069        serializer.serialize_str(self.as_str())
1070    }
1071}
1072
1073impl<'de> Deserialize<'de> for ToolResultRemedyKind {
1074    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1075        let s = String::deserialize(deserializer)?;
1076        Ok(Self::from(s.as_str()))
1077    }
1078}
1079
1080/// Whose login a remedy names ([`ToolResultRemedy::provider`]).
1081#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1082pub enum RemedyLoginProvider {
1083    /// The claude.ai account (`/login`).
1084    ClaudeAi,
1085    /// Claude Design's own authorization (`/design login`).
1086    ClaudeDesign,
1087    /// A provider not yet known to this version of the crate.
1088    Unknown(String),
1089}
1090
1091impl RemedyLoginProvider {
1092    pub fn as_str(&self) -> &str {
1093        match self {
1094            Self::ClaudeAi => "claude_ai",
1095            Self::ClaudeDesign => "claude_design",
1096            Self::Unknown(s) => s.as_str(),
1097        }
1098    }
1099}
1100
1101impl fmt::Display for RemedyLoginProvider {
1102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1103        f.write_str(self.as_str())
1104    }
1105}
1106
1107impl From<&str> for RemedyLoginProvider {
1108    fn from(s: &str) -> Self {
1109        match s {
1110            "claude_ai" => Self::ClaudeAi,
1111            "claude_design" => Self::ClaudeDesign,
1112            other => Self::Unknown(other.to_string()),
1113        }
1114    }
1115}
1116
1117impl Serialize for RemedyLoginProvider {
1118    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1119        serializer.serialize_str(self.as_str())
1120    }
1121}
1122
1123impl<'de> Deserialize<'de> for RemedyLoginProvider {
1124    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1125        let s = String::deserialize(deserializer)?;
1126        Ok(Self::from(s.as_str()))
1127    }
1128}
1129
1130/// The feature a `feature_disabled` / `policy_denied` remedy names
1131/// ([`ToolResultRemedy::feature`]).
1132#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1133pub enum RemedyFeature {
1134    Workflows,
1135    Artifacts,
1136    /// A feature not yet known to this version of the crate.
1137    Unknown(String),
1138}
1139
1140impl RemedyFeature {
1141    pub fn as_str(&self) -> &str {
1142        match self {
1143            Self::Workflows => "workflows",
1144            Self::Artifacts => "artifacts",
1145            Self::Unknown(s) => s.as_str(),
1146        }
1147    }
1148}
1149
1150impl fmt::Display for RemedyFeature {
1151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1152        f.write_str(self.as_str())
1153    }
1154}
1155
1156impl From<&str> for RemedyFeature {
1157    fn from(s: &str) -> Self {
1158        match s {
1159            "workflows" => Self::Workflows,
1160            "artifacts" => Self::Artifacts,
1161            other => Self::Unknown(other.to_string()),
1162        }
1163    }
1164}
1165
1166impl Serialize for RemedyFeature {
1167    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1168        serializer.serialize_str(self.as_str())
1169    }
1170}
1171
1172impl<'de> Deserialize<'de> for RemedyFeature {
1173    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1174        let s = String::deserialize(deserializer)?;
1175        Ok(Self::from(s.as_str()))
1176    }
1177}
1178
1179/// Why a feature is off for a `feature_disabled` remedy
1180/// ([`ToolResultRemedy::cause`]).
1181#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1182pub enum RemedyFeatureCause {
1183    /// `CLAUDE_CODE_DISABLE_WORKFLOWS`, or `disableWorkflows` set by a
1184    /// settings layer above the user's own.
1185    ManagedSettings,
1186    /// An organization policy.
1187    OrgPolicy,
1188    /// Not launched for this account.
1189    Unavailable,
1190    /// The user's own `/config` setting is off (including a
1191    /// `disableWorkflows` in their own settings).
1192    UserSetting,
1193    /// This Claude Code process is not running on Bun, the runtime its
1194    /// binary is built with, and workflow scripts run only there
1195    /// (CLI 2.1.307+).
1196    UnsupportedRuntime,
1197    /// A cause not yet known to this version of the crate.
1198    Unknown(String),
1199}
1200
1201impl RemedyFeatureCause {
1202    pub fn as_str(&self) -> &str {
1203        match self {
1204            Self::ManagedSettings => "managed_settings",
1205            Self::OrgPolicy => "org_policy",
1206            Self::Unavailable => "unavailable",
1207            Self::UserSetting => "user_setting",
1208            Self::UnsupportedRuntime => "unsupported_runtime",
1209            Self::Unknown(s) => s.as_str(),
1210        }
1211    }
1212}
1213
1214impl fmt::Display for RemedyFeatureCause {
1215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1216        f.write_str(self.as_str())
1217    }
1218}
1219
1220impl From<&str> for RemedyFeatureCause {
1221    fn from(s: &str) -> Self {
1222        match s {
1223            "managed_settings" => Self::ManagedSettings,
1224            "org_policy" => Self::OrgPolicy,
1225            "unavailable" => Self::Unavailable,
1226            "user_setting" => Self::UserSetting,
1227            "unsupported_runtime" => Self::UnsupportedRuntime,
1228            other => Self::Unknown(other.to_string()),
1229        }
1230    }
1231}
1232
1233impl Serialize for RemedyFeatureCause {
1234    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1235        serializer.serialize_str(self.as_str())
1236    }
1237}
1238
1239impl<'de> Deserialize<'de> for RemedyFeatureCause {
1240    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1241        let s = String::deserialize(deserializer)?;
1242        Ok(Self::from(s.as_str()))
1243    }
1244}
1245
1246/// The verdict behind a `policy_denied` remedy
1247/// ([`ToolResultRemedy::policy_kind`]).
1248#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1249pub enum RemedyPolicyKind {
1250    /// The organization's policy turns the feature off.
1251    OrgDenied,
1252    /// A HIPAA-regulated organization signed in earlier in this process;
1253    /// restarting clears it.
1254    Latched,
1255    /// A verdict not yet known to this version of the crate.
1256    Unknown(String),
1257}
1258
1259impl RemedyPolicyKind {
1260    pub fn as_str(&self) -> &str {
1261        match self {
1262            Self::OrgDenied => "org_denied",
1263            Self::Latched => "latched",
1264            Self::Unknown(s) => s.as_str(),
1265        }
1266    }
1267}
1268
1269impl fmt::Display for RemedyPolicyKind {
1270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1271        f.write_str(self.as_str())
1272    }
1273}
1274
1275impl From<&str> for RemedyPolicyKind {
1276    fn from(s: &str) -> Self {
1277        match s {
1278            "org_denied" => Self::OrgDenied,
1279            "latched" => Self::Latched,
1280            other => Self::Unknown(other.to_string()),
1281        }
1282    }
1283}
1284
1285impl Serialize for RemedyPolicyKind {
1286    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1287        serializer.serialize_str(self.as_str())
1288    }
1289}
1290
1291impl<'de> Deserialize<'de> for RemedyPolicyKind {
1292    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1293        let s = String::deserialize(deserializer)?;
1294        Ok(Self::from(s.as_str()))
1295    }
1296}
1297
1298/// User message
1299#[derive(Debug, Clone, Serialize, Deserialize)]
1300pub struct UserMessage {
1301    pub message: MessageContent,
1302    #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
1303    #[serde(
1304        serialize_with = "serialize_optional_uuid",
1305        deserialize_with = "deserialize_optional_uuid"
1306    )]
1307    pub session_id: Option<Uuid>,
1308    /// Parent tool use ID for nested agent messages
1309    #[serde(skip_serializing_if = "Option::is_none")]
1310    pub parent_tool_use_id: Option<String>,
1311    /// Message-level unique identifier
1312    #[serde(skip_serializing_if = "Option::is_none")]
1313    pub uuid: Option<String>,
1314    /// CLI-emitted ISO-8601 timestamp for the message (present on echoed tool results).
1315    #[serde(skip_serializing_if = "Option::is_none")]
1316    pub timestamp: Option<String>,
1317    /// Structured tool result data echoed by the CLI alongside the `tool_result`
1318    /// content block. The shape depends on which tool produced it (e.g. for
1319    /// `AskUserQuestion` it is `{ questions, answers }`; for `Bash` it is
1320    /// `{ stdout, stderr, exit_code, ... }`). Stored as raw JSON to preserve
1321    /// wire fidelity; use [`UserMessage::tool_use_result_as`] to parse into a
1322    /// typed shape when you know which tool was invoked.
1323    #[serde(skip_serializing_if = "Option::is_none")]
1324    pub tool_use_result: Option<serde_json::Value>,
1325    /// ID of the subagent this user message belongs to (the `task_id` of its
1326    /// task events), on the prompt echoed into a `local_agent` subagent.
1327    /// Seen on the wire from CLI 2.1.292, where it is not part of the
1328    /// published user-message schema. Not settable by clients.
1329    #[serde(default, skip_serializing_if = "Option::is_none")]
1330    pub agent_id: Option<String>,
1331    /// Subagent type, when this user message is the prompt echoed into a
1332    /// `local_agent` subagent (e.g. `general-purpose`).
1333    #[serde(skip_serializing_if = "Option::is_none")]
1334    pub subagent_type: Option<String>,
1335    /// Short description of the subagent task, present alongside `subagent_type`.
1336    #[serde(skip_serializing_if = "Option::is_none")]
1337    pub task_description: Option<String>,
1338    #[serde(skip_serializing_if = "Option::is_none")]
1339    pub origin: Option<MessageOrigin>,
1340    #[serde(skip_serializing_if = "Option::is_none")]
1341    pub priority: Option<String>,
1342    #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
1343    pub is_synthetic: Option<bool>,
1344    #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
1345    pub should_query: Option<bool>,
1346    #[serde(default, skip_serializing_if = "Option::is_none")]
1347    pub is_meta: Option<bool>,
1348    #[serde(default, skip_serializing_if = "Option::is_none")]
1349    pub is_visible_in_transcript_only: Option<bool>,
1350    #[serde(default, skip_serializing_if = "Option::is_none")]
1351    pub is_virtual: Option<bool>,
1352    #[serde(default, skip_serializing_if = "Option::is_none")]
1353    pub is_compact_summary: Option<bool>,
1354    #[serde(skip_serializing_if = "Option::is_none")]
1355    pub summarize_metadata: Option<SummarizeMetadata>,
1356    #[serde(skip_serializing_if = "Option::is_none")]
1357    pub mcp_meta: Option<McpMeta>,
1358    /// Display metadata for this message's `tool_result` blocks, keyed by
1359    /// `tool_use_id`.
1360    #[serde(skip_serializing_if = "Option::is_none")]
1361    pub tool_result_meta: Option<Vec<ToolResultMeta>>,
1362    #[serde(skip_serializing_if = "Option::is_none")]
1363    pub source_tool_use_id: Option<String>,
1364    #[serde(skip_serializing_if = "Option::is_none")]
1365    pub source_tool_assistant_uuid: Option<String>,
1366    #[serde(skip_serializing_if = "Option::is_none")]
1367    pub image_paste_ids: Option<Vec<u64>>,
1368    #[serde(skip_serializing_if = "Option::is_none")]
1369    pub client_platform: Option<String>,
1370    #[serde(skip_serializing_if = "Option::is_none")]
1371    pub inbound_origin: Option<String>,
1372    #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
1373    pub is_replay: Option<bool>,
1374    #[serde(skip_serializing_if = "Option::is_none")]
1375    pub file_attachments: Option<Vec<Value>>,
1376    /// Desktop host only: the host's own seeded summon (CLI 2.1.239+).
1377    #[serde(default, skip_serializing_if = "Option::is_none")]
1378    pub seeded_summon: Option<bool>,
1379    /// True when the client composed this turn from content the user did not
1380    /// type; its text is delivered as written (CLI 2.1.259+).
1381    #[serde(default, skip_serializing_if = "Option::is_none")]
1382    pub client_composed: Option<bool>,
1383    /// True when no person typed this message's text in this session (a
1384    /// server or an app composed it). The text is delivered as written: no
1385    /// slash-command dispatch, no at-mention expansion beyond the message's
1386    /// own uploaded files, no keyword reminders or commands, and a skill it
1387    /// names does not count as typed by the user. Unlike
1388    /// [`client_composed`](Self::client_composed), the turn-start attachment
1389    /// pass still runs. Who sent it stays with [`origin`](Self::origin)
1390    /// (CLI 2.1.300+).
1391    #[serde(default, skip_serializing_if = "Option::is_none")]
1392    pub text_not_typed: Option<bool>,
1393    /// The host's own label for what started this turn (e.g. the desktop
1394    /// app's `scheduled-task`, `sleep-auto-resume`); `user`, an empty string
1395    /// or absent means a person sent it. For usage attribution; on CLI
1396    /// 2.1.317+ any other label also keeps a message with no origin, sent
1397    /// while a turn is running, from going out as the user's words
1398    /// (CLI 2.1.278+).
1399    #[serde(default, skip_serializing_if = "Option::is_none")]
1400    pub initiator: Option<String>,
1401    /// Content the user pasted into the prompt rather than typed: each entry
1402    /// a string or an array of content blocks. The CLI appends the text of
1403    /// each entry after the typed text, in order, and may wrap it in
1404    /// `<pasted_content>` tags. Blocks other than text are ignored; send
1405    /// images and documents in `message.content` (CLI 2.1.278+).
1406    #[serde(default, skip_serializing_if = "Option::is_none")]
1407    pub pasted_content: Option<Vec<Value>>,
1408    /// Replayed history rather than a live message: the Remote Control
1409    /// bridge stamps it on the messages it flushes to the session server,
1410    /// which also stamps it on deliveries it replays (CLI 2.1.266+).
1411    #[serde(default, skip_serializing_if = "Option::is_none")]
1412    pub historical: Option<bool>,
1413    /// Epoch milliseconds on the session server's clock when it received the
1414    /// HTTP request that delivered this message. Written by the session
1415    /// server; the CLI copies it to the turn's result as
1416    /// [`user_message_server_received_wall_ms`](crate::io::ResultMessage::user_message_server_received_wall_ms)
1417    /// (CLI 2.1.286+). Not settable by clients.
1418    #[serde(default, skip_serializing_if = "Option::is_none")]
1419    pub server_received_wall_ms: Option<f64>,
1420    /// Only on the synthetic message that re-asks the same model after a
1421    /// refusal: the API `msg_*` id of the refused response. Absent when the
1422    /// CLI has no well-formed id for it (CLI 2.1.290+).
1423    #[serde(default, skip_serializing_if = "Option::is_none")]
1424    pub refused_message_id: Option<String>,
1425    /// Input only: asks the CLI to put this prompt back at the front of its
1426    /// queue, under the same uuid, when an interrupt without `cancel_queued`
1427    /// stops the turn taking it in within this many milliseconds (counted
1428    /// from the turn's `system/status` `requesting` frame) and before the
1429    /// model's reply begins. Values above 10000 count as 10000
1430    /// (CLI 2.1.307+).
1431    #[serde(default, skip_serializing_if = "Option::is_none")]
1432    pub requeue_if_interrupted_within_ms: Option<u64>,
1433    /// Input only: the sender's statement that the person typed `ultrathink`
1434    /// in this prompt and was shown the word colored before sending, and
1435    /// that nothing in the prompt arrived whole (no paste, no share, no
1436    /// prompt the sender suggested). Send it only as `Some(true)`, and only
1437    /// while the session's newest `system/init` carried
1438    /// [`ultrathink_typed_raises`](crate::io::InitMessage::ultrathink_typed_raises)
1439    /// `true`. The turn then runs at `xhigh` effort if the CLI's own checks
1440    /// agree; it is the sender's claim, not proof (CLI 2.1.317+).
1441    #[serde(default, skip_serializing_if = "Option::is_none")]
1442    pub ultrathink_typed: Option<bool>,
1443    /// Input only: the MCP App views' states a remote client sent with this
1444    /// prompt, replacing the set the worker holds (`[]` clears it). Entries
1445    /// are opaque (CLI 2.1.317+).
1446    #[serde(default, skip_serializing_if = "Option::is_none")]
1447    pub tool_states: Option<Vec<Value>>,
1448    /// Output only, and to be read only on a message the CLI wrote (on a
1449    /// session event, only where its `source` is `worker`). On a message a
1450    /// plugin sent, what a host can show under the plugin's name in place
1451    /// of `message`. Absent on any other message, and on a prompt sent as
1452    /// the person's own words; there, show `message` (CLI 2.1.317+).
1453    #[serde(default, skip_serializing_if = "Option::is_none")]
1454    pub plugin_prompt: Option<PluginPrompt>,
1455}
1456
1457/// [`UserMessage::plugin_prompt`]: what a host can show for a message a
1458/// plugin sent (CLI 2.1.317+).
1459#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1460pub struct PluginPrompt {
1461    /// The plugin's name as the wrapper the model reads prints it, on one
1462    /// line and scrubbed; `origin.name` holds it raw.
1463    pub name: String,
1464    /// The prompt as hooks left it (prompts merged into one turn joined by
1465    /// newlines) when `message` is exactly the wrapper the model reads
1466    /// around that prompt; or the command the plugin ran as a person would
1467    /// type it, such as `/review 12`, when `message` is only that command's
1468    /// tags. Can be more than one line; show all of it.
1469    pub text: String,
1470}
1471
1472impl UserMessage {
1473    /// Parse the `tool_use_result` field into a caller-specified type.
1474    ///
1475    /// Returns `None` if `tool_use_result` is absent, otherwise returns the
1476    /// deserialization result. The caller must know which tool produced the
1477    /// result and supply a matching type — e.g. for `AskUserQuestion` use
1478    /// [`AskUserQuestionInput`](crate::AskUserQuestionInput), whose
1479    /// `questions` + `answers` fields match the wire result shape.
1480    pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
1481        &self,
1482    ) -> Option<Result<T, serde_json::Error>> {
1483        self.tool_use_result
1484            .as_ref()
1485            .map(|v| serde_json::from_value(v.clone()))
1486    }
1487
1488    /// Parse the `tool_use_result` as a subagent (`Task`) run result.
1489    ///
1490    /// When this user message echoes the result of a `Task` tool call, the CLI
1491    /// attaches a structured `tool_use_result` carrying the subagent's token,
1492    /// timing, and tool-use accounting. Returns `None` when the field is absent
1493    /// or does not parse as a [`SubagentResult`].
1494    ///
1495    /// Summing [`SubagentResult::total_tokens`] across every `Task` result in a
1496    /// session yields the subagent token rollup the CLI renders as
1497    /// `subagent_tokens` in its terminal `<usage>` block.
1498    pub fn subagent_result(&self) -> Option<SubagentResult> {
1499        self.tool_use_result
1500            .as_ref()
1501            .and_then(|v| serde_json::from_value(v.clone()).ok())
1502    }
1503}
1504
1505/// Token, timing, and tool-use accounting for a completed subagent (`Task`) run.
1506///
1507/// The Claude CLI echoes this object in the `tool_use_result` of a `Task` tool's
1508/// result message. It is the typed source of truth for subagent token
1509/// attribution: the per-run [`total_tokens`](Self::total_tokens),
1510/// [`total_duration_ms`](Self::total_duration_ms), and
1511/// [`total_tool_use_count`](Self::total_tool_use_count) correspond to the
1512/// `subagent_tokens` / `duration_ms` / `tool_uses` line items the CLI renders in
1513/// its human-readable `<usage>` block, and [`usage`](Self::usage) carries the
1514/// full per-model token breakdown for the run.
1515#[derive(Debug, Clone, Serialize, Deserialize)]
1516pub struct SubagentResult {
1517    /// Completion status of the subagent run (e.g. `"completed"`).
1518    #[serde(skip_serializing_if = "Option::is_none")]
1519    pub status: Option<String>,
1520    /// The prompt the subagent was launched with.
1521    #[serde(skip_serializing_if = "Option::is_none")]
1522    pub prompt: Option<String>,
1523    /// Stable identifier of the spawned subagent.
1524    #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
1525    pub agent_id: Option<String>,
1526    /// Subagent type that ran (e.g. `general-purpose`, `Explore`).
1527    #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
1528    pub agent_type: Option<String>,
1529    /// Final content blocks the subagent returned.
1530    #[serde(
1531        default,
1532        deserialize_with = "deserialize_content_blocks",
1533        skip_serializing_if = "Vec::is_empty"
1534    )]
1535    pub content: Vec<ContentBlock>,
1536    /// Model the subagent actually resolved to (e.g. `claude-sonnet-4-6`).
1537    #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
1538    pub resolved_model: Option<String>,
1539    /// Wall-clock duration of the subagent run, in milliseconds.
1540    #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
1541    pub total_duration_ms: Option<u64>,
1542    /// Total tokens consumed by the subagent — the `subagent_tokens` rollup line.
1543    #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
1544    pub total_tokens: Option<u64>,
1545    /// Number of tool invocations the subagent made.
1546    #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
1547    pub total_tool_use_count: Option<u64>,
1548    /// Detailed token / cache usage for the subagent run.
1549    #[serde(skip_serializing_if = "Option::is_none")]
1550    pub usage: Option<super::result::UsageInfo>,
1551    /// Per-category tool-use counts, present for some agent types (e.g. `Explore`).
1552    #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
1553    pub tool_stats: Option<SubagentToolStats>,
1554}
1555
1556/// Per-category tool-use counts for a subagent run, from `tool_use_result.toolStats`.
1557///
1558/// The `extra` field captures any counters the CLI adds that aren't modeled here,
1559/// so new wire fields deserialize without error.
1560#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1561#[serde(rename_all = "camelCase")]
1562pub struct SubagentToolStats {
1563    #[serde(default)]
1564    pub read_count: u64,
1565    #[serde(default)]
1566    pub search_count: u64,
1567    #[serde(default)]
1568    pub bash_count: u64,
1569    #[serde(default)]
1570    pub edit_file_count: u64,
1571    #[serde(default)]
1572    pub lines_added: u64,
1573    #[serde(default)]
1574    pub lines_removed: u64,
1575    #[serde(default)]
1576    pub other_tool_count: u64,
1577    #[serde(flatten)]
1578    pub extra: serde_json::Map<String, Value>,
1579}
1580
1581/// Session-level subagent token rollup — the `<subagent_tokens>` /
1582/// `<agent_count>` line items the Claude CLI renders in its terminal
1583/// `<usage>` block.
1584///
1585/// The `stream-json` protocol does **not** carry this rollup on the `result`
1586/// frame's `usage` (confirmed against the CLI binary — the terminal renderer
1587/// computes it from `Task` tool results). Consumers that need it must
1588/// accumulate it the same way: feed every session message through
1589/// [`observe`](Self::observe) and read the totals at any point.
1590///
1591/// A `Task` result observed twice under the same `agentId` (e.g. a replayed
1592/// frame on resume) is counted once. Results with no `agentId` are counted
1593/// every time they are observed.
1594///
1595/// # Example
1596///
1597/// ```
1598/// use claude_codes::{ClaudeOutput, SubagentUsageRollup};
1599///
1600/// let mut rollup = SubagentUsageRollup::default();
1601/// let json = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"status":"completed","agentId":"ab52f22445470d454","totalDurationMs":1853,"totalTokens":10201,"totalToolUseCount":0}}"#;
1602/// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1603/// rollup.observe(&output);
1604/// assert_eq!(rollup.subagent_tokens, 10201);
1605/// assert_eq!(rollup.agent_count, 1);
1606/// ```
1607#[derive(Debug, Clone, Default, PartialEq, Eq)]
1608pub struct SubagentUsageRollup {
1609    /// Total tokens consumed by subagents — sum of
1610    /// [`SubagentResult::total_tokens`] over every observed `Task` result.
1611    pub subagent_tokens: u64,
1612    /// Number of subagent runs observed (`<agent_count>`).
1613    pub agent_count: u32,
1614    /// Total subagent tool invocations — sum of `total_tool_use_count`.
1615    pub tool_uses: u64,
1616    /// Total subagent wall-clock milliseconds — sum of `total_duration_ms`.
1617    pub duration_ms: u64,
1618    seen_agent_ids: std::collections::BTreeSet<String>,
1619}
1620
1621impl SubagentUsageRollup {
1622    /// Accumulate `output` into the rollup if it is a `Task` tool result.
1623    ///
1624    /// Returns `true` when the message contributed to the totals. Non-user
1625    /// messages, user messages without a `tool_use_result`, results from
1626    /// other tools, and duplicate `agentId`s are all ignored.
1627    pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
1628        match output {
1629            ClaudeOutput::User(user) => self.observe_user(user),
1630            _ => false,
1631        }
1632    }
1633
1634    /// Accumulate a user message's `Task` tool result, if it carries one.
1635    ///
1636    /// Every [`SubagentResult`] field is optional, so any JSON object in
1637    /// `tool_use_result` parses as one (e.g. a `Bash` or `ToolSearch`
1638    /// result). Only results carrying an `agentId` or a `totalTokens`
1639    /// line item are treated as genuine `Task` results.
1640    pub fn observe_user(&mut self, user: &UserMessage) -> bool {
1641        let Some(result) = user.subagent_result() else {
1642            return false;
1643        };
1644        if result.total_tokens.is_none() && result.agent_id.is_none() {
1645            return false;
1646        }
1647        if let Some(agent_id) = &result.agent_id {
1648            if !self.seen_agent_ids.insert(agent_id.clone()) {
1649                return false;
1650            }
1651        }
1652        self.agent_count += 1;
1653        self.subagent_tokens += result.total_tokens.unwrap_or(0);
1654        self.tool_uses += result.total_tool_use_count.unwrap_or(0);
1655        self.duration_ms += result.total_duration_ms.unwrap_or(0);
1656        true
1657    }
1658}
1659
1660/// Message content with role
1661#[derive(Debug, Clone, Serialize, Deserialize)]
1662pub struct MessageContent {
1663    pub role: MessageRole,
1664    #[serde(deserialize_with = "deserialize_content_blocks")]
1665    pub content: Vec<ContentBlock>,
1666}
1667
1668/// System message with metadata
1669#[derive(Debug, Clone, Serialize, Deserialize)]
1670pub struct SystemMessage {
1671    pub subtype: SystemSubtype,
1672    #[serde(flatten)]
1673    pub data: Value, // Captures all other fields
1674}
1675
1676impl SystemMessage {
1677    /// Check if this is an init message
1678    pub fn is_init(&self) -> bool {
1679        self.subtype == SystemSubtype::Init
1680    }
1681
1682    /// Check if this is a status message
1683    pub fn is_status(&self) -> bool {
1684        self.subtype == SystemSubtype::Status
1685    }
1686
1687    /// Check if this is a compact_boundary message
1688    pub fn is_compact_boundary(&self) -> bool {
1689        self.subtype == SystemSubtype::CompactBoundary
1690    }
1691
1692    /// Try to parse as an init message
1693    pub fn as_init(&self) -> Option<InitMessage> {
1694        if self.subtype != SystemSubtype::Init {
1695            return None;
1696        }
1697        serde_json::from_value(self.data.clone()).ok()
1698    }
1699
1700    /// Try to parse as a status message
1701    pub fn as_status(&self) -> Option<StatusMessage> {
1702        if self.subtype != SystemSubtype::Status {
1703            return None;
1704        }
1705        serde_json::from_value(self.data.clone()).ok()
1706    }
1707
1708    /// Try to parse as a compact_boundary message
1709    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
1710        if self.subtype != SystemSubtype::CompactBoundary {
1711            return None;
1712        }
1713        serde_json::from_value(self.data.clone()).ok()
1714    }
1715
1716    /// Check if this is a task_started message
1717    pub fn is_task_started(&self) -> bool {
1718        self.subtype == SystemSubtype::TaskStarted
1719    }
1720
1721    /// Check if this is a task_progress message
1722    pub fn is_task_progress(&self) -> bool {
1723        self.subtype == SystemSubtype::TaskProgress
1724    }
1725
1726    /// Check if this is a task_notification message
1727    pub fn is_task_notification(&self) -> bool {
1728        self.subtype == SystemSubtype::TaskNotification
1729    }
1730
1731    /// Try to parse as a task_started message
1732    pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
1733        if self.subtype != SystemSubtype::TaskStarted {
1734            return None;
1735        }
1736        serde_json::from_value(self.data.clone()).ok()
1737    }
1738
1739    /// Try to parse as a task_progress message
1740    pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
1741        if self.subtype != SystemSubtype::TaskProgress {
1742            return None;
1743        }
1744        serde_json::from_value(self.data.clone()).ok()
1745    }
1746
1747    /// Try to parse as a task_notification message
1748    pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
1749        if self.subtype != SystemSubtype::TaskNotification {
1750            return None;
1751        }
1752        serde_json::from_value(self.data.clone()).ok()
1753    }
1754
1755    /// Check if this is a task_updated message
1756    pub fn is_task_updated(&self) -> bool {
1757        self.subtype == SystemSubtype::TaskUpdated
1758    }
1759
1760    /// Try to parse as a task_updated message
1761    pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
1762        if self.subtype != SystemSubtype::TaskUpdated {
1763            return None;
1764        }
1765        serde_json::from_value(self.data.clone()).ok()
1766    }
1767
1768    /// Check if this is a thinking_tokens message
1769    pub fn is_thinking_tokens(&self) -> bool {
1770        self.subtype == SystemSubtype::ThinkingTokens
1771    }
1772
1773    /// Try to parse as a thinking_tokens message
1774    pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
1775        if self.subtype != SystemSubtype::ThinkingTokens {
1776            return None;
1777        }
1778        serde_json::from_value(self.data.clone()).ok()
1779    }
1780
1781    /// Check if this is a code_change_published message
1782    pub fn is_code_change_published(&self) -> bool {
1783        self.subtype == SystemSubtype::CodeChangePublished
1784    }
1785
1786    /// Try to parse as a code_change_published message
1787    pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
1788        if self.subtype != SystemSubtype::CodeChangePublished {
1789            return None;
1790        }
1791        serde_json::from_value(self.data.clone()).ok()
1792    }
1793
1794    /// Check if this is a vcs_state_changed message
1795    pub fn is_vcs_state_changed(&self) -> bool {
1796        self.subtype == SystemSubtype::VcsStateChanged
1797    }
1798
1799    /// Try to parse as a vcs_state_changed message
1800    pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
1801        if self.subtype != SystemSubtype::VcsStateChanged {
1802            return None;
1803        }
1804        serde_json::from_value(self.data.clone()).ok()
1805    }
1806
1807    /// Check if this is a feedback_draft_queued message.
1808    pub fn is_feedback_draft_queued(&self) -> bool {
1809        self.subtype == SystemSubtype::FeedbackDraftQueued
1810    }
1811
1812    /// Try to parse as a feedback_draft_queued message.
1813    pub fn as_feedback_draft_queued(&self) -> Option<FeedbackDraftQueuedMessage> {
1814        if self.subtype != SystemSubtype::FeedbackDraftQueued {
1815            return None;
1816        }
1817        serde_json::from_value(self.data.clone()).ok()
1818    }
1819
1820    /// Check if this is a cloud_session_delta message.
1821    pub fn is_cloud_session_delta(&self) -> bool {
1822        self.subtype == SystemSubtype::CloudSessionDelta
1823    }
1824
1825    /// Try to parse as a cloud_session_delta message.
1826    pub fn as_cloud_session_delta(&self) -> Option<CloudSessionDeltaMessage> {
1827        if self.subtype != SystemSubtype::CloudSessionDelta {
1828            return None;
1829        }
1830        serde_json::from_value(self.data.clone()).ok()
1831    }
1832
1833    /// Check if this is a dev_intent message.
1834    pub fn is_dev_intent(&self) -> bool {
1835        self.subtype == SystemSubtype::DevIntent
1836    }
1837
1838    /// Try to parse as a dev_intent message.
1839    pub fn as_dev_intent(&self) -> Option<DevIntentMessage> {
1840        if self.subtype != SystemSubtype::DevIntent {
1841            return None;
1842        }
1843        serde_json::from_value(self.data.clone()).ok()
1844    }
1845
1846    /// Check if this is a turn_handoff_available message.
1847    pub fn is_turn_handoff_available(&self) -> bool {
1848        self.subtype == SystemSubtype::TurnHandoffAvailable
1849    }
1850
1851    /// Try to parse as a turn_handoff_available message.
1852    pub fn as_turn_handoff_available(&self) -> Option<TurnHandoffAvailableMessage> {
1853        if self.subtype != SystemSubtype::TurnHandoffAvailable {
1854            return None;
1855        }
1856        serde_json::from_value(self.data.clone()).ok()
1857    }
1858
1859    /// Check if this is a turn_preempted message.
1860    pub fn is_turn_preempted(&self) -> bool {
1861        self.subtype == SystemSubtype::TurnPreempted
1862    }
1863
1864    /// Try to parse as a turn_preempted message.
1865    pub fn as_turn_preempted(&self) -> Option<TurnPreemptedMessage> {
1866        if self.subtype != SystemSubtype::TurnPreempted {
1867            return None;
1868        }
1869        serde_json::from_value(self.data.clone()).ok()
1870    }
1871
1872    /// Check if this is a peer_message_hold message.
1873    pub fn is_peer_message_hold(&self) -> bool {
1874        self.subtype == SystemSubtype::PeerMessageHold
1875    }
1876
1877    /// Try to parse as a peer_message_hold message.
1878    pub fn as_peer_message_hold(&self) -> Option<PeerMessageHoldMessage> {
1879        if self.subtype != SystemSubtype::PeerMessageHold {
1880            return None;
1881        }
1882        serde_json::from_value(self.data.clone()).ok()
1883    }
1884
1885    /// Check if this is a per_turn_effort_changed message.
1886    pub fn is_per_turn_effort_changed(&self) -> bool {
1887        self.subtype == SystemSubtype::PerTurnEffortChanged
1888    }
1889
1890    /// Try to parse as a per_turn_effort_changed message.
1891    pub fn as_per_turn_effort_changed(&self) -> Option<PerTurnEffortChangedMessage> {
1892        if self.subtype != SystemSubtype::PerTurnEffortChanged {
1893            return None;
1894        }
1895        serde_json::from_value(self.data.clone()).ok()
1896    }
1897
1898    /// Check if this is a session_title_changed message.
1899    pub fn is_session_title_changed(&self) -> bool {
1900        self.subtype == SystemSubtype::SessionTitleChanged
1901    }
1902
1903    /// Try to parse as a session_title_changed message.
1904    pub fn as_session_title_changed(&self) -> Option<SessionTitleChangedMessage> {
1905        if self.subtype != SystemSubtype::SessionTitleChanged {
1906            return None;
1907        }
1908        serde_json::from_value(self.data.clone()).ok()
1909    }
1910
1911    /// Check if this is a session_writes_lost message.
1912    pub fn is_session_writes_lost(&self) -> bool {
1913        self.subtype == SystemSubtype::SessionWritesLost
1914    }
1915
1916    /// Try to parse as a session_writes_lost message.
1917    pub fn as_session_writes_lost(&self) -> Option<SessionWritesLostMessage> {
1918        if self.subtype != SystemSubtype::SessionWritesLost {
1919            return None;
1920        }
1921        serde_json::from_value(self.data.clone()).ok()
1922    }
1923
1924    /// Check if this is an instruction_size_warning message.
1925    pub fn is_instruction_size_warning(&self) -> bool {
1926        self.subtype == SystemSubtype::InstructionSizeWarning
1927    }
1928
1929    /// Try to parse as an instruction_size_warning message.
1930    pub fn as_instruction_size_warning(&self) -> Option<InstructionSizeWarningMessage> {
1931        if self.subtype != SystemSubtype::InstructionSizeWarning {
1932            return None;
1933        }
1934        serde_json::from_value(self.data.clone()).ok()
1935    }
1936
1937    /// Check if this is a permission_check_status message.
1938    pub fn is_permission_check_status(&self) -> bool {
1939        self.subtype == SystemSubtype::PermissionCheckStatus
1940    }
1941
1942    /// Try to parse as a permission_check_status message.
1943    pub fn as_permission_check_status(&self) -> Option<PermissionCheckStatusMessage> {
1944        if self.subtype != SystemSubtype::PermissionCheckStatus {
1945            return None;
1946        }
1947        serde_json::from_value(self.data.clone()).ok()
1948    }
1949
1950    /// Check if this is an inherited_asks_settled message.
1951    pub fn is_inherited_asks_settled(&self) -> bool {
1952        self.subtype == SystemSubtype::InheritedAsksSettled
1953    }
1954
1955    /// Try to parse as an inherited_asks_settled message.
1956    pub fn as_inherited_asks_settled(&self) -> Option<InheritedAsksSettledMessage> {
1957        if self.subtype != SystemSubtype::InheritedAsksSettled {
1958            return None;
1959        }
1960        serde_json::from_value(self.data.clone()).ok()
1961    }
1962
1963    /// Check if this is a file_attachments_missing message.
1964    pub fn is_file_attachments_missing(&self) -> bool {
1965        self.subtype == SystemSubtype::FileAttachmentsMissing
1966    }
1967
1968    /// Try to parse as a file_attachments_missing message.
1969    pub fn as_file_attachments_missing(&self) -> Option<FileAttachmentsMissingMessage> {
1970        if self.subtype != SystemSubtype::FileAttachmentsMissing {
1971            return None;
1972        }
1973        serde_json::from_value(self.data.clone()).ok()
1974    }
1975
1976    /// Check if this is a ui_focus message.
1977    pub fn is_ui_focus(&self) -> bool {
1978        self.subtype == SystemSubtype::UiFocus
1979    }
1980
1981    /// Try to parse as a ui_focus message.
1982    pub fn as_ui_focus(&self) -> Option<UiFocusMessage> {
1983        if self.subtype != SystemSubtype::UiFocus {
1984            return None;
1985        }
1986        serde_json::from_value(self.data.clone()).ok()
1987    }
1988
1989    /// Check if this is a ui_invalidate message.
1990    pub fn is_ui_invalidate(&self) -> bool {
1991        self.subtype == SystemSubtype::UiInvalidate
1992    }
1993
1994    /// Try to parse as a ui_invalidate message.
1995    pub fn as_ui_invalidate(&self) -> Option<UiInvalidateMessage> {
1996        if self.subtype != SystemSubtype::UiInvalidate {
1997            return None;
1998        }
1999        serde_json::from_value(self.data.clone()).ok()
2000    }
2001
2002    /// Check if this is a ui_log message.
2003    pub fn is_ui_log(&self) -> bool {
2004        self.subtype == SystemSubtype::UiLog
2005    }
2006
2007    /// Try to parse as a ui_log message.
2008    pub fn as_ui_log(&self) -> Option<UiLogMessage> {
2009        if self.subtype != SystemSubtype::UiLog {
2010            return None;
2011        }
2012        serde_json::from_value(self.data.clone()).ok()
2013    }
2014
2015    /// Check if this is a ui_notify message.
2016    pub fn is_ui_notify(&self) -> bool {
2017        self.subtype == SystemSubtype::UiNotify
2018    }
2019
2020    /// Try to parse as a ui_notify message.
2021    pub fn as_ui_notify(&self) -> Option<UiNotifyMessage> {
2022        if self.subtype != SystemSubtype::UiNotify {
2023            return None;
2024        }
2025        serde_json::from_value(self.data.clone()).ok()
2026    }
2027
2028    /// Check if this is a ui_notify_cancel message.
2029    pub fn is_ui_notify_cancel(&self) -> bool {
2030        self.subtype == SystemSubtype::UiNotifyCancel
2031    }
2032
2033    /// Try to parse as a ui_notify_cancel message.
2034    pub fn as_ui_notify_cancel(&self) -> Option<UiNotifyCancelMessage> {
2035        if self.subtype != SystemSubtype::UiNotifyCancel {
2036            return None;
2037        }
2038        serde_json::from_value(self.data.clone()).ok()
2039    }
2040
2041    /// Check if this is a ui_panes message.
2042    pub fn is_ui_panes(&self) -> bool {
2043        self.subtype == SystemSubtype::UiPanes
2044    }
2045
2046    /// Try to parse as a ui_panes message.
2047    pub fn as_ui_panes(&self) -> Option<UiPanesMessage> {
2048        if self.subtype != SystemSubtype::UiPanes {
2049            return None;
2050        }
2051        serde_json::from_value(self.data.clone()).ok()
2052    }
2053
2054    /// Check if this is a ui_placed message.
2055    pub fn is_ui_placed(&self) -> bool {
2056        self.subtype == SystemSubtype::UiPlaced
2057    }
2058
2059    /// Try to parse as a ui_placed message.
2060    pub fn as_ui_placed(&self) -> Option<UiPlacedMessage> {
2061        if self.subtype != SystemSubtype::UiPlaced {
2062            return None;
2063        }
2064        serde_json::from_value(self.data.clone()).ok()
2065    }
2066
2067    /// Check if this is a ui_scroll message.
2068    pub fn is_ui_scroll(&self) -> bool {
2069        self.subtype == SystemSubtype::UiScroll
2070    }
2071
2072    /// Try to parse as a ui_scroll message.
2073    pub fn as_ui_scroll(&self) -> Option<UiScrollMessage> {
2074        if self.subtype != SystemSubtype::UiScroll {
2075            return None;
2076        }
2077        serde_json::from_value(self.data.clone()).ok()
2078    }
2079
2080    /// Check if this is a ui_status message.
2081    pub fn is_ui_status(&self) -> bool {
2082        self.subtype == SystemSubtype::UiStatus
2083    }
2084
2085    /// Try to parse as a ui_status message.
2086    pub fn as_ui_status(&self) -> Option<UiStatusMessage> {
2087        if self.subtype != SystemSubtype::UiStatus {
2088            return None;
2089        }
2090        serde_json::from_value(self.data.clone()).ok()
2091    }
2092
2093    /// Check if this is a ui_toast message.
2094    pub fn is_ui_toast(&self) -> bool {
2095        self.subtype == SystemSubtype::UiToast
2096    }
2097
2098    /// Try to parse as a ui_toast message.
2099    pub fn as_ui_toast(&self) -> Option<UiToastMessage> {
2100        if self.subtype != SystemSubtype::UiToast {
2101            return None;
2102        }
2103        serde_json::from_value(self.data.clone()).ok()
2104    }
2105
2106    /// Parse any typed system subtype known to this crate version.
2107    pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
2108        macro_rules! parse {
2109            ($variant:ident, $ty:ty) => {
2110                serde_json::from_value::<$ty>(self.data.clone())
2111                    .ok()
2112                    .map(KnownSystemEvent::$variant)
2113            };
2114        }
2115
2116        match self.subtype {
2117            SystemSubtype::Init => parse!(Init, InitMessage),
2118            SystemSubtype::Status => parse!(Status, StatusMessage),
2119            SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
2120            SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
2121            SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
2122            SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
2123            SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
2124            SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
2125            SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
2126            SystemSubtype::ControlRequestProgress => {
2127                parse!(ControlRequestProgress, ControlRequestProgressMessage)
2128            }
2129            SystemSubtype::ModelRefusalFallback => {
2130                parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
2131            }
2132            SystemSubtype::ModelRefusalNoFallback => {
2133                parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
2134            }
2135            SystemSubtype::LocalCommandOutput => {
2136                parse!(LocalCommandOutput, LocalCommandOutputMessage)
2137            }
2138            SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
2139            SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
2140            SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
2141            SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
2142            SystemSubtype::BackgroundTasksChanged => {
2143                parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
2144            }
2145            SystemSubtype::SessionStateChanged => {
2146                parse!(SessionStateChanged, SessionStateChangedMessage)
2147            }
2148            SystemSubtype::WorkerShuttingDown => {
2149                parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
2150            }
2151            SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
2152            SystemSubtype::Notification => parse!(Notification, NotificationMessage),
2153            SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
2154            SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
2155            SystemSubtype::ElicitationComplete => {
2156                parse!(ElicitationComplete, ElicitationCompleteMessage)
2157            }
2158            SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
2159            SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
2160            SystemSubtype::Informational => parse!(Informational, InformationalMessage),
2161            SystemSubtype::CodeChangePublished => {
2162                parse!(CodeChangePublished, CodeChangePublishedMessage)
2163            }
2164            SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
2165            SystemSubtype::FeedbackDraftQueued => {
2166                parse!(FeedbackDraftQueued, FeedbackDraftQueuedMessage)
2167            }
2168            SystemSubtype::CloudSessionDelta => {
2169                parse!(CloudSessionDelta, CloudSessionDeltaMessage)
2170            }
2171            SystemSubtype::DevIntent => parse!(DevIntent, DevIntentMessage),
2172            SystemSubtype::TurnHandoffAvailable => {
2173                parse!(TurnHandoffAvailable, TurnHandoffAvailableMessage)
2174            }
2175            SystemSubtype::TurnPreempted => parse!(TurnPreempted, TurnPreemptedMessage),
2176            SystemSubtype::PeerMessageHold => parse!(PeerMessageHold, PeerMessageHoldMessage),
2177            SystemSubtype::PerTurnEffortChanged => {
2178                parse!(PerTurnEffortChanged, PerTurnEffortChangedMessage)
2179            }
2180            SystemSubtype::SessionTitleChanged => {
2181                parse!(SessionTitleChanged, SessionTitleChangedMessage)
2182            }
2183            SystemSubtype::SessionWritesLost => {
2184                parse!(SessionWritesLost, SessionWritesLostMessage)
2185            }
2186            SystemSubtype::InstructionSizeWarning => {
2187                parse!(InstructionSizeWarning, InstructionSizeWarningMessage)
2188            }
2189            SystemSubtype::FileAttachmentsMissing => {
2190                parse!(FileAttachmentsMissing, FileAttachmentsMissingMessage)
2191            }
2192            SystemSubtype::PermissionCheckStatus => {
2193                parse!(PermissionCheckStatus, PermissionCheckStatusMessage)
2194            }
2195            SystemSubtype::InheritedAsksSettled => {
2196                parse!(InheritedAsksSettled, InheritedAsksSettledMessage)
2197            }
2198            SystemSubtype::UiFocus => parse!(UiFocus, UiFocusMessage),
2199            SystemSubtype::UiInvalidate => parse!(UiInvalidate, UiInvalidateMessage),
2200            SystemSubtype::UiLog => parse!(UiLog, UiLogMessage),
2201            SystemSubtype::UiNotify => parse!(UiNotify, UiNotifyMessage),
2202            SystemSubtype::UiNotifyCancel => parse!(UiNotifyCancel, UiNotifyCancelMessage),
2203            SystemSubtype::UiPanes => parse!(UiPanes, UiPanesMessage),
2204            SystemSubtype::UiPlaced => parse!(UiPlaced, UiPlacedMessage),
2205            SystemSubtype::UiScroll => parse!(UiScroll, UiScrollMessage),
2206            SystemSubtype::UiStatus => parse!(UiStatus, UiStatusMessage),
2207            SystemSubtype::UiToast => parse!(UiToast, UiToastMessage),
2208            SystemSubtype::Unknown(_) => None,
2209        }
2210    }
2211
2212    /// Re-serialize this system message's payload through the typed view that
2213    /// matches its `subtype`, returning the result as JSON.
2214    ///
2215    /// Used by the wrapping audit ([`crate::io::audit_frame`]) to verify that a
2216    /// subtype's dedicated struct captures every wire field: the audit compares
2217    /// this against the raw [`SystemMessage::data`]. Returns `None` for subtypes
2218    /// this crate version has no dedicated struct for (including
2219    /// [`SystemSubtype::Unknown`]) — those are reported as not fully wrapped.
2220    pub fn typed_value(&self) -> Option<Value> {
2221        fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
2222            parsed.and_then(|v| serde_json::to_value(v).ok())
2223        }
2224        match self.subtype {
2225            SystemSubtype::Init => reserialize(self.as_init()),
2226            SystemSubtype::Status => reserialize(self.as_status()),
2227            SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
2228            SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
2229            SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
2230            SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
2231            SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
2232            SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
2233            SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
2234            SystemSubtype::ControlRequestProgress => {
2235                reserialize(parse_system::<ControlRequestProgressMessage>(self))
2236            }
2237            SystemSubtype::ModelRefusalFallback => {
2238                reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
2239            }
2240            SystemSubtype::ModelRefusalNoFallback => {
2241                reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
2242            }
2243            SystemSubtype::LocalCommandOutput => {
2244                reserialize(parse_system::<LocalCommandOutputMessage>(self))
2245            }
2246            SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
2247            SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
2248            SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
2249            SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
2250            SystemSubtype::BackgroundTasksChanged => {
2251                reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
2252            }
2253            SystemSubtype::SessionStateChanged => {
2254                reserialize(parse_system::<SessionStateChangedMessage>(self))
2255            }
2256            SystemSubtype::WorkerShuttingDown => {
2257                reserialize(parse_system::<WorkerShuttingDownMessage>(self))
2258            }
2259            SystemSubtype::CommandsChanged => {
2260                reserialize(parse_system::<CommandsChangedMessage>(self))
2261            }
2262            SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
2263            SystemSubtype::FilesPersisted => {
2264                reserialize(parse_system::<FilesPersistedMessage>(self))
2265            }
2266            SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
2267            SystemSubtype::ElicitationComplete => {
2268                reserialize(parse_system::<ElicitationCompleteMessage>(self))
2269            }
2270            SystemSubtype::PermissionDenied => {
2271                reserialize(parse_system::<PermissionDeniedMessage>(self))
2272            }
2273            SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
2274            SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
2275            SystemSubtype::CodeChangePublished => {
2276                reserialize(parse_system::<CodeChangePublishedMessage>(self))
2277            }
2278            SystemSubtype::VcsStateChanged => {
2279                reserialize(parse_system::<VcsStateChangedMessage>(self))
2280            }
2281            SystemSubtype::FeedbackDraftQueued => {
2282                reserialize(parse_system::<FeedbackDraftQueuedMessage>(self))
2283            }
2284            SystemSubtype::CloudSessionDelta => {
2285                reserialize(parse_system::<CloudSessionDeltaMessage>(self))
2286            }
2287            SystemSubtype::DevIntent => reserialize(parse_system::<DevIntentMessage>(self)),
2288            SystemSubtype::TurnHandoffAvailable => {
2289                reserialize(parse_system::<TurnHandoffAvailableMessage>(self))
2290            }
2291            SystemSubtype::TurnPreempted => reserialize(parse_system::<TurnPreemptedMessage>(self)),
2292            SystemSubtype::PeerMessageHold => {
2293                reserialize(parse_system::<PeerMessageHoldMessage>(self))
2294            }
2295            SystemSubtype::PerTurnEffortChanged => {
2296                reserialize(parse_system::<PerTurnEffortChangedMessage>(self))
2297            }
2298            SystemSubtype::SessionTitleChanged => {
2299                reserialize(parse_system::<SessionTitleChangedMessage>(self))
2300            }
2301            SystemSubtype::SessionWritesLost => {
2302                reserialize(parse_system::<SessionWritesLostMessage>(self))
2303            }
2304            SystemSubtype::InstructionSizeWarning => {
2305                reserialize(parse_system::<InstructionSizeWarningMessage>(self))
2306            }
2307            SystemSubtype::FileAttachmentsMissing => {
2308                reserialize(parse_system::<FileAttachmentsMissingMessage>(self))
2309            }
2310            SystemSubtype::PermissionCheckStatus => {
2311                reserialize(parse_system::<PermissionCheckStatusMessage>(self))
2312            }
2313            SystemSubtype::InheritedAsksSettled => {
2314                reserialize(parse_system::<InheritedAsksSettledMessage>(self))
2315            }
2316            SystemSubtype::UiFocus => reserialize(parse_system::<UiFocusMessage>(self)),
2317            SystemSubtype::UiInvalidate => reserialize(parse_system::<UiInvalidateMessage>(self)),
2318            SystemSubtype::UiLog => reserialize(parse_system::<UiLogMessage>(self)),
2319            SystemSubtype::UiNotify => reserialize(parse_system::<UiNotifyMessage>(self)),
2320            SystemSubtype::UiNotifyCancel => {
2321                reserialize(parse_system::<UiNotifyCancelMessage>(self))
2322            }
2323            SystemSubtype::UiPanes => reserialize(parse_system::<UiPanesMessage>(self)),
2324            SystemSubtype::UiPlaced => reserialize(parse_system::<UiPlacedMessage>(self)),
2325            SystemSubtype::UiScroll => reserialize(parse_system::<UiScrollMessage>(self)),
2326            SystemSubtype::UiStatus => reserialize(parse_system::<UiStatusMessage>(self)),
2327            SystemSubtype::UiToast => reserialize(parse_system::<UiToastMessage>(self)),
2328            SystemSubtype::Unknown(_) => None,
2329        }
2330    }
2331}
2332
2333fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
2334    serde_json::from_value(message.data.clone()).ok()
2335}
2336
2337/// Owned typed view over any known system message subtype.
2338// `InitMessage` outgrew clippy's variant-size threshold when CLI 2.1.232
2339// added fields. This enum is a transient per-parse classification (never
2340// stored in bulk), so boxing would break every match site for no retained-
2341// memory win.
2342#[allow(clippy::large_enum_variant)]
2343#[derive(Debug, Clone, Serialize, Deserialize)]
2344pub enum KnownSystemEvent {
2345    Init(InitMessage),
2346    Status(StatusMessage),
2347    CompactBoundary(CompactBoundaryMessage),
2348    ThinkingTokens(ThinkingTokensMessage),
2349    TaskStarted(TaskStartedMessage),
2350    TaskProgress(TaskProgressMessage),
2351    TaskUpdated(TaskUpdatedMessage),
2352    TaskNotification(TaskNotificationMessage),
2353    ApiRetry(ApiRetryMessage),
2354    ControlRequestProgress(ControlRequestProgressMessage),
2355    ModelRefusalFallback(ModelRefusalFallbackMessage),
2356    ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
2357    LocalCommandOutput(LocalCommandOutputMessage),
2358    HookStarted(HookStartedMessage),
2359    HookProgress(HookProgressMessage),
2360    HookResponse(HookResponseMessage),
2361    PluginInstall(PluginInstallMessage),
2362    BackgroundTasksChanged(BackgroundTasksChangedMessage),
2363    SessionStateChanged(SessionStateChangedMessage),
2364    WorkerShuttingDown(WorkerShuttingDownMessage),
2365    CommandsChanged(CommandsChangedMessage),
2366    Notification(NotificationMessage),
2367    FilesPersisted(FilesPersistedMessage),
2368    MemoryRecall(MemoryRecallMessage),
2369    ElicitationComplete(ElicitationCompleteMessage),
2370    PermissionDenied(PermissionDeniedMessage),
2371    MirrorError(MirrorErrorMessage),
2372    Informational(InformationalMessage),
2373    CodeChangePublished(CodeChangePublishedMessage),
2374    VcsStateChanged(VcsStateChangedMessage),
2375    FeedbackDraftQueued(FeedbackDraftQueuedMessage),
2376    CloudSessionDelta(CloudSessionDeltaMessage),
2377    DevIntent(DevIntentMessage),
2378    TurnHandoffAvailable(TurnHandoffAvailableMessage),
2379    TurnPreempted(TurnPreemptedMessage),
2380    PeerMessageHold(PeerMessageHoldMessage),
2381    PerTurnEffortChanged(PerTurnEffortChangedMessage),
2382    SessionTitleChanged(SessionTitleChangedMessage),
2383    SessionWritesLost(SessionWritesLostMessage),
2384    InstructionSizeWarning(InstructionSizeWarningMessage),
2385    FileAttachmentsMissing(FileAttachmentsMissingMessage),
2386    PermissionCheckStatus(PermissionCheckStatusMessage),
2387    InheritedAsksSettled(InheritedAsksSettledMessage),
2388    UiFocus(UiFocusMessage),
2389    UiInvalidate(UiInvalidateMessage),
2390    UiLog(UiLogMessage),
2391    UiNotify(UiNotifyMessage),
2392    UiNotifyCancel(UiNotifyCancelMessage),
2393    UiPanes(UiPanesMessage),
2394    UiPlaced(UiPlacedMessage),
2395    UiScroll(UiScrollMessage),
2396    UiStatus(UiStatusMessage),
2397    UiToast(UiToastMessage),
2398}
2399
2400#[derive(Debug, Clone, Serialize, Deserialize)]
2401pub struct ApiRetryMessage {
2402    pub attempt: u64,
2403    pub max_retries: u64,
2404    pub retry_delay_ms: u64,
2405    pub error_status: Option<u16>,
2406    pub error: String,
2407    /// Present only when the API sent no response headers within the
2408    /// first-byte window (`CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS`). For this
2409    /// cause `max_retries` is its own cap (normally one retry), not the
2410    /// session budget (CLI 2.1.261+).
2411    #[serde(default, skip_serializing_if = "Option::is_none")]
2412    pub no_response: Option<ApiRetryNoResponse>,
2413    #[serde(default, skip_serializing_if = "Option::is_none")]
2414    pub uuid: Option<String>,
2415    #[serde(default, skip_serializing_if = "Option::is_none")]
2416    pub session_id: Option<String>,
2417}
2418
2419/// Timing of a first-byte-timeout retry, carried as
2420/// [`ApiRetryMessage::no_response`].
2421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2422pub struct ApiRetryNoResponse {
2423    /// How long the failed attempt waited for response headers.
2424    pub waited_ms: u64,
2425    /// How long the retry will wait for them.
2426    pub retry_wait_ms: u64,
2427}
2428
2429#[derive(Debug, Clone, Serialize, Deserialize)]
2430pub struct ControlRequestProgressMessage {
2431    pub request_id: String,
2432    pub status: String,
2433    #[serde(default, skip_serializing_if = "Option::is_none")]
2434    pub attempt: Option<u64>,
2435    #[serde(default, skip_serializing_if = "Option::is_none")]
2436    pub max_retries: Option<u64>,
2437    #[serde(default, skip_serializing_if = "Option::is_none")]
2438    pub retry_delay_ms: Option<u64>,
2439    #[serde(default, skip_serializing_if = "Option::is_none")]
2440    pub error_status: Option<u16>,
2441    #[serde(default, skip_serializing_if = "Option::is_none")]
2442    pub error: Option<String>,
2443    #[serde(default, skip_serializing_if = "Option::is_none")]
2444    pub uuid: Option<String>,
2445    #[serde(default, skip_serializing_if = "Option::is_none")]
2446    pub session_id: Option<String>,
2447}
2448
2449#[derive(Debug, Clone, Serialize, Deserialize)]
2450pub struct ModelRefusalFallbackMessage {
2451    pub trigger: String,
2452    pub direction: String,
2453    /// `"session"`: the main thread fell back and the session model is
2454    /// swapped. `"local"`: a subagent / side-question (`/btw`) / background
2455    /// fork fell back — only that response came from the fallback model and
2456    /// the session model is unchanged. Absent from CLIs before 2.1.222
2457    /// (treat as `"session"`).
2458    #[serde(default, skip_serializing_if = "Option::is_none")]
2459    pub scope: Option<RefusalFallbackScope>,
2460    pub original_model: String,
2461    pub fallback_model: String,
2462    pub request_id: Option<String>,
2463    #[serde(default, skip_serializing_if = "Option::is_none")]
2464    pub api_refusal_category: Option<String>,
2465    /// Present when any hop of this banner's multi-hop episode was a cyber
2466    /// refusal — not only the origin hop `api_refusal_category` describes.
2467    /// Re-arm evidence for the CLI's cyber-exclusion header on session
2468    /// restore; absent on cyber-free episodes and older CLIs (2.1.239+).
2469    #[serde(default, skip_serializing_if = "Option::is_none")]
2470    pub saw_cyber_refusal: Option<bool>,
2471    #[serde(default, skip_serializing_if = "Option::is_none")]
2472    pub api_refusal_explanation: Option<String>,
2473    /// Cyber Verification Program status of the account that made the
2474    /// request, as the API gave it for `original_model` (`stop_details.
2475    /// cyber_verification`). Only on session-scope notices whose
2476    /// `api_refusal_category` is `cyber`; it may not belong to `request_id`
2477    /// when the switch went through several hops. Absent, or `{type:
2478    /// "inactive"}` with no reason, means unknown, not "no access". Map
2479    /// known values to your own text; never display them (CLI 2.1.317+).
2480    #[serde(default, skip_serializing_if = "Option::is_none")]
2481    pub api_refusal_cyber_verification: Option<CyberVerificationStatus>,
2482    #[serde(default, skip_serializing_if = "Option::is_none")]
2483    pub retracted_message_uuids: Option<Vec<String>>,
2484    #[serde(default, skip_serializing_if = "Option::is_none")]
2485    pub refused_user_message_uuid: Option<String>,
2486    pub content: Value,
2487    #[serde(default, skip_serializing_if = "Option::is_none")]
2488    pub uuid: Option<String>,
2489    #[serde(default, skip_serializing_if = "Option::is_none")]
2490    pub session_id: Option<String>,
2491}
2492
2493/// [`ModelRefusalFallbackMessage::api_refusal_cyber_verification`]: the
2494/// account's Cyber Verification Program status as the API reported it
2495/// (CLI 2.1.317+). All three values are opaque API strings.
2496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2497pub struct CyberVerificationStatus {
2498    #[serde(rename = "type")]
2499    pub verification_type: String,
2500    #[serde(default, skip_serializing_if = "Option::is_none")]
2501    pub tier: Option<String>,
2502    #[serde(default, skip_serializing_if = "Option::is_none")]
2503    pub reason: Option<String>,
2504}
2505
2506/// Scope of a refusal-fallback model swap, carried by
2507/// [`ModelRefusalFallbackMessage::scope`]. Open — new scopes may ship on the
2508/// wire ahead of schema updates.
2509#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2510pub enum RefusalFallbackScope {
2511    /// The main thread fell back; the session model is swapped.
2512    Session,
2513    /// A subagent / side-question / background fork fell back; only that
2514    /// response used the fallback model, the session model is unchanged.
2515    Local,
2516    /// A scope not yet known to this version of the crate.
2517    Unknown(String),
2518}
2519
2520impl RefusalFallbackScope {
2521    pub fn as_str(&self) -> &str {
2522        match self {
2523            Self::Session => "session",
2524            Self::Local => "local",
2525            Self::Unknown(s) => s.as_str(),
2526        }
2527    }
2528}
2529
2530impl fmt::Display for RefusalFallbackScope {
2531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2532        f.write_str(self.as_str())
2533    }
2534}
2535
2536impl From<&str> for RefusalFallbackScope {
2537    fn from(s: &str) -> Self {
2538        match s {
2539            "session" => Self::Session,
2540            "local" => Self::Local,
2541            other => Self::Unknown(other.to_string()),
2542        }
2543    }
2544}
2545
2546impl Serialize for RefusalFallbackScope {
2547    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2548        serializer.serialize_str(self.as_str())
2549    }
2550}
2551
2552impl<'de> Deserialize<'de> for RefusalFallbackScope {
2553    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2554        let s = String::deserialize(deserializer)?;
2555        Ok(Self::from(s.as_str()))
2556    }
2557}
2558
2559#[derive(Debug, Clone, Serialize, Deserialize)]
2560pub struct ModelRefusalNoFallbackMessage {
2561    pub original_model: String,
2562    pub request_id: Option<String>,
2563    #[serde(default, skip_serializing_if = "Option::is_none")]
2564    pub api_refusal_category: Option<String>,
2565    #[serde(default, skip_serializing_if = "Option::is_none")]
2566    pub api_refusal_explanation: Option<String>,
2567    pub content: Value,
2568    #[serde(default, skip_serializing_if = "Option::is_none")]
2569    pub uuid: Option<String>,
2570    #[serde(default, skip_serializing_if = "Option::is_none")]
2571    pub session_id: Option<String>,
2572}
2573
2574#[derive(Debug, Clone, Serialize, Deserialize)]
2575pub struct LocalCommandOutputMessage {
2576    pub content: String,
2577    #[serde(default, skip_serializing_if = "Option::is_none")]
2578    pub uuid: Option<String>,
2579    #[serde(default, skip_serializing_if = "Option::is_none")]
2580    pub session_id: Option<String>,
2581}
2582
2583#[derive(Debug, Clone, Serialize, Deserialize)]
2584pub struct HookStartedMessage {
2585    pub hook_id: String,
2586    pub hook_name: String,
2587    pub hook_event: String,
2588    #[serde(default, skip_serializing_if = "Option::is_none")]
2589    pub uuid: Option<String>,
2590    #[serde(default, skip_serializing_if = "Option::is_none")]
2591    pub session_id: Option<String>,
2592}
2593
2594#[derive(Debug, Clone, Serialize, Deserialize)]
2595pub struct HookProgressMessage {
2596    pub hook_id: String,
2597    pub hook_name: String,
2598    pub hook_event: String,
2599    #[serde(default, skip_serializing_if = "Option::is_none")]
2600    pub stdout: Option<String>,
2601    #[serde(default, skip_serializing_if = "Option::is_none")]
2602    pub stderr: Option<String>,
2603    #[serde(default, skip_serializing_if = "Option::is_none")]
2604    pub output: Option<String>,
2605    #[serde(default, skip_serializing_if = "Option::is_none")]
2606    pub uuid: Option<String>,
2607    #[serde(default, skip_serializing_if = "Option::is_none")]
2608    pub session_id: Option<String>,
2609}
2610
2611#[derive(Debug, Clone, Serialize, Deserialize)]
2612pub struct HookResponseMessage {
2613    pub hook_id: String,
2614    pub hook_name: String,
2615    pub hook_event: String,
2616    #[serde(default, skip_serializing_if = "Option::is_none")]
2617    pub stdout: Option<String>,
2618    #[serde(default, skip_serializing_if = "Option::is_none")]
2619    pub stderr: Option<String>,
2620    #[serde(default, skip_serializing_if = "Option::is_none")]
2621    pub output: Option<String>,
2622    #[serde(default, skip_serializing_if = "Option::is_none")]
2623    pub exit_code: Option<i32>,
2624    pub outcome: String,
2625    #[serde(default, skip_serializing_if = "Option::is_none")]
2626    pub uuid: Option<String>,
2627    #[serde(default, skip_serializing_if = "Option::is_none")]
2628    pub session_id: Option<String>,
2629}
2630
2631#[derive(Debug, Clone, Serialize, Deserialize)]
2632pub struct PluginInstallMessage {
2633    pub status: String,
2634    #[serde(default, skip_serializing_if = "Option::is_none")]
2635    pub name: Option<String>,
2636    #[serde(default, skip_serializing_if = "Option::is_none")]
2637    pub error: Option<String>,
2638    #[serde(default, skip_serializing_if = "Option::is_none")]
2639    pub uuid: Option<String>,
2640    #[serde(default, skip_serializing_if = "Option::is_none")]
2641    pub session_id: Option<String>,
2642}
2643
2644#[derive(Debug, Clone, Serialize, Deserialize)]
2645pub struct BackgroundTasksChangedMessage {
2646    pub tasks: Vec<BackgroundTaskInfo>,
2647    #[serde(default, skip_serializing_if = "Option::is_none")]
2648    pub uuid: Option<String>,
2649    #[serde(default, skip_serializing_if = "Option::is_none")]
2650    pub session_id: Option<String>,
2651}
2652
2653#[derive(Debug, Clone, Serialize, Deserialize)]
2654pub struct BackgroundTaskInfo {
2655    pub task_id: String,
2656    /// Id of the task's current run; see [`TaskStartedMessage::run_id`]
2657    /// (CLI 2.1.292+).
2658    #[serde(default, skip_serializing_if = "Option::is_none")]
2659    pub run_id: Option<String>,
2660    pub task_type: String,
2661    /// Agent type of a `local_agent` task, such as `general-purpose` or a
2662    /// custom agent's name; `main-session` for a backgrounded main session.
2663    /// Not set on other tasks (CLI 2.1.293+).
2664    #[serde(default, skip_serializing_if = "Option::is_none")]
2665    pub subagent_type: Option<String>,
2666    pub description: String,
2667    /// `task_id` of the subagent task that launched this one; see
2668    /// [`TaskStartedMessage::parent_task_id`] (CLI 2.1.292+).
2669    #[serde(default, skip_serializing_if = "Option::is_none")]
2670    pub parent_task_id: Option<String>,
2671    /// True for housekeeping tasks the CLI does not surface as user work;
2672    /// hosts should exclude them from activity indicators (CLI 2.1.259+).
2673    #[serde(default, skip_serializing_if = "Option::is_none")]
2674    pub ambient: Option<bool>,
2675    /// True for an awaited run of a resumed subagent; see
2676    /// [`TaskStartedMessage::awaited`] (CLI 2.1.293+).
2677    #[serde(default, skip_serializing_if = "Option::is_none")]
2678    pub awaited: Option<bool>,
2679}
2680
2681#[derive(Debug, Clone, Serialize, Deserialize)]
2682pub struct SessionStateChangedMessage {
2683    pub state: String,
2684    #[serde(default, skip_serializing_if = "Option::is_none")]
2685    pub uuid: Option<String>,
2686    #[serde(default, skip_serializing_if = "Option::is_none")]
2687    pub session_id: Option<String>,
2688}
2689
2690#[derive(Debug, Clone, Serialize, Deserialize)]
2691pub struct WorkerShuttingDownMessage {
2692    pub reason: String,
2693    #[serde(default, skip_serializing_if = "Option::is_none")]
2694    pub uuid: Option<String>,
2695    #[serde(default, skip_serializing_if = "Option::is_none")]
2696    pub session_id: Option<String>,
2697}
2698
2699#[derive(Debug, Clone, Serialize, Deserialize)]
2700pub struct CommandsChangedMessage {
2701    pub commands: Vec<CommandInfo>,
2702    #[serde(default, skip_serializing_if = "Option::is_none")]
2703    pub uuid: Option<String>,
2704    #[serde(default, skip_serializing_if = "Option::is_none")]
2705    pub session_id: Option<String>,
2706}
2707
2708#[derive(Debug, Clone, Serialize, Deserialize)]
2709pub struct CommandInfo {
2710    pub name: String,
2711    pub description: String,
2712    #[serde(rename = "argumentHint")]
2713    pub argument_hint: String,
2714    #[serde(default, skip_serializing_if = "Option::is_none")]
2715    pub aliases: Option<Vec<String>>,
2716    /// True when the command is Claude Code's own; absent for a command
2717    /// defined by a user, project, plugin or MCP server. Rows can share a
2718    /// name: when a marked row carries it, `/name` runs that one, and an
2719    /// unmarked row is the one `/name` runs only when no marked row shares
2720    /// its name (CLI 2.1.278+).
2721    #[serde(default, skip_serializing_if = "Option::is_none")]
2722    pub builtin: Option<bool>,
2723}
2724
2725#[derive(Debug, Clone, Serialize, Deserialize)]
2726pub struct NotificationMessage {
2727    pub key: String,
2728    pub text: String,
2729    pub priority: String,
2730    #[serde(default, skip_serializing_if = "Option::is_none")]
2731    pub color: Option<String>,
2732    #[serde(default, skip_serializing_if = "Option::is_none")]
2733    pub timeout_ms: Option<u64>,
2734    #[serde(default, skip_serializing_if = "Option::is_none")]
2735    pub uuid: Option<String>,
2736    #[serde(default, skip_serializing_if = "Option::is_none")]
2737    pub session_id: Option<String>,
2738}
2739
2740#[derive(Debug, Clone, Serialize, Deserialize)]
2741pub struct FilesPersistedMessage {
2742    pub files: Vec<PersistedFile>,
2743    pub failed: Vec<FailedPersistedFile>,
2744    pub processed_at: String,
2745    #[serde(default, skip_serializing_if = "Option::is_none")]
2746    pub uuid: Option<String>,
2747    #[serde(default, skip_serializing_if = "Option::is_none")]
2748    pub session_id: Option<String>,
2749}
2750
2751#[derive(Debug, Clone, Serialize, Deserialize)]
2752pub struct PersistedFile {
2753    pub filename: String,
2754    pub file_id: String,
2755}
2756
2757#[derive(Debug, Clone, Serialize, Deserialize)]
2758pub struct FailedPersistedFile {
2759    pub filename: String,
2760    pub error: String,
2761}
2762
2763#[derive(Debug, Clone, Serialize, Deserialize)]
2764pub struct MemoryRecallMessage {
2765    pub mode: String,
2766    pub memories: Vec<MemoryRecallItem>,
2767    #[serde(default, skip_serializing_if = "Option::is_none")]
2768    pub uuid: Option<String>,
2769    #[serde(default, skip_serializing_if = "Option::is_none")]
2770    pub session_id: Option<String>,
2771}
2772
2773#[derive(Debug, Clone, Serialize, Deserialize)]
2774pub struct MemoryRecallItem {
2775    pub path: String,
2776    pub scope: String,
2777    #[serde(default, skip_serializing_if = "Option::is_none")]
2778    pub content: Option<String>,
2779}
2780
2781#[derive(Debug, Clone, Serialize, Deserialize)]
2782pub struct ElicitationCompleteMessage {
2783    pub mcp_server_name: String,
2784    pub elicitation_id: String,
2785    #[serde(default, skip_serializing_if = "Option::is_none")]
2786    pub uuid: Option<String>,
2787    #[serde(default, skip_serializing_if = "Option::is_none")]
2788    pub session_id: Option<String>,
2789}
2790
2791#[derive(Debug, Clone, Serialize, Deserialize)]
2792pub struct PermissionDeniedMessage {
2793    pub tool_name: String,
2794    pub tool_use_id: String,
2795    #[serde(default, skip_serializing_if = "Option::is_none")]
2796    pub agent_id: Option<String>,
2797    #[serde(default, skip_serializing_if = "Option::is_none")]
2798    pub decision_reason_type: Option<String>,
2799    /// A closed-set code for a reason a host can act on, beside
2800    /// `decision_reason_type` (whose values are unchanged). Absent for
2801    /// every other reason; new values are additive (CLI 2.1.276+).
2802    #[serde(default, skip_serializing_if = "Option::is_none")]
2803    pub decision_reason_code: Option<PermissionDeniedReasonCode>,
2804    #[serde(default, skip_serializing_if = "Option::is_none")]
2805    pub decision_reason: Option<String>,
2806    pub message: String,
2807    #[serde(default, skip_serializing_if = "Option::is_none")]
2808    pub uuid: Option<String>,
2809    #[serde(default, skip_serializing_if = "Option::is_none")]
2810    pub session_id: Option<String>,
2811}
2812
2813/// An actionable reason behind a `system/permission_denied` frame
2814/// ([`PermissionDeniedMessage::decision_reason_code`]).
2815#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2816pub enum PermissionDeniedReasonCode {
2817    /// The auto-mode classifier's transcript exceeded its context window.
2818    ClassifierTranscriptTooLong,
2819    /// `permissions.blockReadsOutsideWorkingDirectories` refused the path.
2820    OutsideReadsBlocked,
2821    /// `/pause-memory` has memory paused.
2822    MemoryPaused,
2823    /// Auto mode's safety check does not run for this session, so calls
2824    /// that need it are refused. Another permission mode can run such a
2825    /// call unless only the check may allow the tool; the check can come
2826    /// back (CLI 2.1.319+).
2827    AutoModeNoCheckInSession,
2828    /// The denial that ended a turn or subagent run after ten responses in
2829    /// a row with no verdict: the check does not run for this session
2830    /// (CLI 2.1.319+).
2831    AutoModeStopNoCheckInSession,
2832    /// As [`AutoModeStopNoCheckInSession`](Self::AutoModeStopNoCheckInSession),
2833    /// but the call got no result from the check (CLI 2.1.319+).
2834    AutoModeStopNoResultForCalls,
2835    /// As [`AutoModeStopNoCheckInSession`](Self::AutoModeStopNoCheckInSession),
2836    /// but the server returned no usable verdict, as in an outage
2837    /// (CLI 2.1.319+).
2838    AutoModeStopNoVerdict,
2839    /// A code not yet known to this version of the crate.
2840    Unknown(String),
2841}
2842
2843impl PermissionDeniedReasonCode {
2844    pub fn as_str(&self) -> &str {
2845        match self {
2846            Self::ClassifierTranscriptTooLong => "classifier_transcript_too_long",
2847            Self::OutsideReadsBlocked => "outside_reads_blocked",
2848            Self::MemoryPaused => "memory_paused",
2849            Self::AutoModeNoCheckInSession => "auto_mode_no_check_in_session",
2850            Self::AutoModeStopNoCheckInSession => "auto_mode_stop_no_check_in_session",
2851            Self::AutoModeStopNoResultForCalls => "auto_mode_stop_no_result_for_calls",
2852            Self::AutoModeStopNoVerdict => "auto_mode_stop_no_verdict",
2853            Self::Unknown(s) => s.as_str(),
2854        }
2855    }
2856}
2857
2858impl fmt::Display for PermissionDeniedReasonCode {
2859    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2860        f.write_str(self.as_str())
2861    }
2862}
2863
2864impl From<&str> for PermissionDeniedReasonCode {
2865    fn from(s: &str) -> Self {
2866        match s {
2867            "classifier_transcript_too_long" => Self::ClassifierTranscriptTooLong,
2868            "outside_reads_blocked" => Self::OutsideReadsBlocked,
2869            "memory_paused" => Self::MemoryPaused,
2870            "auto_mode_no_check_in_session" => Self::AutoModeNoCheckInSession,
2871            "auto_mode_stop_no_check_in_session" => Self::AutoModeStopNoCheckInSession,
2872            "auto_mode_stop_no_result_for_calls" => Self::AutoModeStopNoResultForCalls,
2873            "auto_mode_stop_no_verdict" => Self::AutoModeStopNoVerdict,
2874            other => Self::Unknown(other.to_string()),
2875        }
2876    }
2877}
2878
2879impl Serialize for PermissionDeniedReasonCode {
2880    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2881        serializer.serialize_str(self.as_str())
2882    }
2883}
2884
2885impl<'de> Deserialize<'de> for PermissionDeniedReasonCode {
2886    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2887        let s = String::deserialize(deserializer)?;
2888        Ok(Self::from(s.as_str()))
2889    }
2890}
2891
2892#[derive(Debug, Clone, Serialize, Deserialize)]
2893pub struct MirrorErrorMessage {
2894    pub error: String,
2895    pub key: MirrorErrorKey,
2896    #[serde(default, skip_serializing_if = "Option::is_none")]
2897    pub uuid: Option<String>,
2898    #[serde(default, skip_serializing_if = "Option::is_none")]
2899    pub session_id: Option<String>,
2900}
2901
2902#[derive(Debug, Clone, Serialize, Deserialize)]
2903pub struct MirrorErrorKey {
2904    #[serde(rename = "projectKey")]
2905    pub project_key: String,
2906    #[serde(rename = "sessionId")]
2907    pub session_id: String,
2908    #[serde(default, skip_serializing_if = "Option::is_none")]
2909    pub subpath: Option<String>,
2910}
2911
2912#[derive(Debug, Clone, Serialize, Deserialize)]
2913pub struct InformationalMessage {
2914    pub content: String,
2915    pub level: String,
2916    #[serde(default, skip_serializing_if = "Option::is_none")]
2917    pub tool_use_id: Option<String>,
2918    #[serde(default, skip_serializing_if = "Option::is_none")]
2919    pub prevent_continuation: Option<bool>,
2920    /// Opaque feature tag on a line a host may treat specially; absent on
2921    /// ordinary lines. Hosts ignore values they do not know (CLI 2.1.288+).
2922    #[serde(default, skip_serializing_if = "Option::is_none")]
2923    pub tag: Option<String>,
2924    /// Present on the line that ends a submitted prompt before a model call:
2925    /// how the prompt ended, so a host can write its own message or count
2926    /// endings without matching `content`. A follow-up a hook blocks while a
2927    /// turn runs carries it too, so read it as the end of a turn only when
2928    /// the next result has `num_turns` 0 (CLI 2.1.300+).
2929    #[serde(default, skip_serializing_if = "Option::is_none")]
2930    pub prompt_submit_outcome: Option<PromptSubmitOutcome>,
2931    /// Present on the warning that says a requested model was refused and
2932    /// the session uses another, so a host can word the notice itself
2933    /// instead of reading `content` (CLI 2.1.317+).
2934    #[serde(default, skip_serializing_if = "Option::is_none")]
2935    pub model_restriction: Option<ModelRestriction>,
2936    #[serde(default, skip_serializing_if = "Option::is_none")]
2937    pub uuid: Option<String>,
2938    #[serde(default, skip_serializing_if = "Option::is_none")]
2939    pub session_id: Option<String>,
2940}
2941
2942/// [`InformationalMessage::model_restriction`]: a requested model was
2943/// refused and the session uses another. Both names are cleaned as they
2944/// are in `content` (model-id characters only, cut to 128 with a trailing
2945/// `…`, or `(unrecognized model name)` when none are left) but are still
2946/// untrusted text: render them as plain text (CLI 2.1.317+).
2947#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2948pub struct ModelRestriction {
2949    /// The refused model as it was asked for, or the id that a refused
2950    /// Default pick stood for.
2951    pub requested_model: String,
2952    /// The model in use when the line was built; a later switch can
2953    /// outdate it.
2954    pub effective_model: String,
2955    /// `start` for the model the process chose when it started (from
2956    /// `--model`, the environment, settings or `--agent`'s frontmatter);
2957    /// `switch` for anything later. The CLI reads an unknown value as
2958    /// absent, so treat one that way too.
2959    #[serde(default, skip_serializing_if = "Option::is_none")]
2960    pub at: Option<String>,
2961}
2962
2963/// How a submitted prompt ended before a model call, carried as
2964/// [`InformationalMessage::prompt_submit_outcome`] (CLI 2.1.300+).
2965#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2966pub struct PromptSubmitOutcome {
2967    /// What ended the prompt. The CLI drops a kind it does not know, so the
2968    /// key can be absent; treat [`PromptSubmitOutcomeKind::Other`] as absent
2969    /// too, and ignore [`reason`](Self::reason) beside it.
2970    #[serde(default, skip_serializing_if = "Option::is_none")]
2971    pub kind: Option<PromptSubmitOutcomeKind>,
2972    /// Sent with kind `no_verdict`, and says why: `remote_run_failed`,
2973    /// `remote_run_timed_out`, `interrupted`, `remote_runner_refused`,
2974    /// `unsupported_in_cloud_session`, `output_distrusted`,
2975    /// `callback_timed_out`, `hook_failed`, `hook_timed_out`,
2976    /// `hooks_never_ran`, `personal_config_refused` or
2977    /// `rewrite_unplaceable`. Open set — treat an unknown value as absent.
2978    #[serde(default, skip_serializing_if = "Option::is_none")]
2979    pub no_verdict_kind: Option<String>,
2980    /// True when the prompt stays in the conversation, where the model reads
2981    /// it on the next turn. Do not infer it from `kind`.
2982    pub prompt_kept: bool,
2983    /// The hook's text as Claude Code got it, sanitized and possibly cut.
2984    /// Never sent with `no_verdict`; absent when Claude Code got no text.
2985    /// Untrusted and can quote the prompt, so render it as plain text.
2986    #[serde(default, skip_serializing_if = "Option::is_none")]
2987    pub reason: Option<String>,
2988}
2989
2990/// [`PromptSubmitOutcome::kind`] — what ended a submitted prompt.
2991#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2992pub enum PromptSubmitOutcomeKind {
2993    /// A hook answered with a block.
2994    BlockedByHook,
2995    /// A hook answered `continue: false`.
2996    StoppedByHook,
2997    /// A plugin's code module dropped the prompt.
2998    DroppedByHook,
2999    /// A plugin's code module answered and did not pass the prompt on.
3000    NotPassedOn,
3001    /// No hook gave a verdict, and Claude Code ended the prompt itself; see
3002    /// [`PromptSubmitOutcome::no_verdict_kind`].
3003    NoVerdict,
3004    /// A kind not yet known to this version of the crate.
3005    Other(String),
3006}
3007
3008impl PromptSubmitOutcomeKind {
3009    pub fn as_str(&self) -> &str {
3010        match self {
3011            Self::BlockedByHook => "blocked_by_hook",
3012            Self::StoppedByHook => "stopped_by_hook",
3013            Self::DroppedByHook => "dropped_by_hook",
3014            Self::NotPassedOn => "not_passed_on",
3015            Self::NoVerdict => "no_verdict",
3016            Self::Other(s) => s.as_str(),
3017        }
3018    }
3019}
3020
3021impl fmt::Display for PromptSubmitOutcomeKind {
3022    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3023        f.write_str(self.as_str())
3024    }
3025}
3026
3027impl From<&str> for PromptSubmitOutcomeKind {
3028    fn from(s: &str) -> Self {
3029        match s {
3030            "blocked_by_hook" => Self::BlockedByHook,
3031            "stopped_by_hook" => Self::StoppedByHook,
3032            "dropped_by_hook" => Self::DroppedByHook,
3033            "not_passed_on" => Self::NotPassedOn,
3034            "no_verdict" => Self::NoVerdict,
3035            other => Self::Other(other.to_string()),
3036        }
3037    }
3038}
3039
3040impl Serialize for PromptSubmitOutcomeKind {
3041    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3042        serializer.serialize_str(self.as_str())
3043    }
3044}
3045
3046impl<'de> Deserialize<'de> for PromptSubmitOutcomeKind {
3047    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3048        let s = String::deserialize(deserializer)?;
3049        Ok(Self::from(s.as_str()))
3050    }
3051}
3052
3053/// Plugin info from the init message
3054#[derive(Debug, Clone, Serialize, Deserialize)]
3055pub struct PluginInfo {
3056    /// Plugin name
3057    pub name: String,
3058    /// Path to the plugin on disk
3059    pub path: String,
3060    /// Plugin registry source (e.g., "rust-analyzer-lsp@claude-plugins-official")
3061    #[serde(skip_serializing_if = "Option::is_none")]
3062    pub source: Option<String>,
3063    /// Installed plugin version (e.g., "1.0.0"). Added in CLI 2.1.219.
3064    #[serde(default, skip_serializing_if = "Option::is_none")]
3065    pub version: Option<String>,
3066}
3067
3068/// Plugin load diagnostic reported by system init.
3069#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3070pub struct PluginDiagnostic {
3071    pub plugin: String,
3072    #[serde(rename = "type")]
3073    pub diagnostic_type: String,
3074    pub message: String,
3075}
3076
3077/// Memory paths reported by system init.
3078#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3079pub struct MemoryPaths {
3080    #[serde(default, skip_serializing_if = "Option::is_none")]
3081    pub auto: Option<String>,
3082    #[serde(default, skip_serializing_if = "Option::is_none")]
3083    pub team: Option<String>,
3084    #[serde(flatten)]
3085    pub extra: serde_json::Map<String, Value>,
3086}
3087
3088/// An MCP server config entry that failed validation, reported by system
3089/// init (e.g. a `url` entry with no `type`). The affected server is skipped
3090/// and absent from `InitMessage::mcp_servers`.
3091#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3092pub struct McpServerError {
3093    pub name: String,
3094    /// Stable error category.
3095    #[serde(rename = "type")]
3096    pub error_type: String,
3097    pub message: String,
3098}
3099
3100/// Init system message data - sent at session start
3101#[derive(Debug, Clone, Serialize, Deserialize)]
3102pub struct InitMessage {
3103    /// Session identifier
3104    pub session_id: String,
3105    /// Current working directory
3106    #[serde(skip_serializing_if = "Option::is_none")]
3107    pub cwd: Option<String>,
3108    /// Model being used
3109    #[serde(skip_serializing_if = "Option::is_none")]
3110    pub model: Option<String>,
3111    /// List of available tools
3112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3113    pub tools: Vec<String>,
3114    /// MCP servers configured
3115    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3116    pub mcp_servers: Vec<Value>,
3117    /// Available slash commands (e.g., "compact", "cost", "review")
3118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3119    pub slash_commands: Vec<String>,
3120    /// Slash commands only meaningful in a terminal context (CLI 2.1.232+,
3121    /// e.g. "doctor", "color")
3122    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3123    pub terminal_slash_commands: Vec<String>,
3124    /// Available agent types (e.g., "Bash", "Explore", "Plan")
3125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3126    pub agents: Vec<String>,
3127    /// Installed plugins
3128    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3129    pub plugins: Vec<PluginInfo>,
3130    /// Installed skills
3131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3132    pub skills: Vec<Value>,
3133    /// Claude Code CLI version
3134    #[serde(skip_serializing_if = "Option::is_none")]
3135    pub claude_code_version: Option<String>,
3136    /// Unix socket path for the harness's inter-session messaging bridge
3137    /// (new in CLI 2.1.232; absent on older CLIs and non-bridged runs)
3138    #[serde(skip_serializing_if = "Option::is_none")]
3139    pub messaging_socket_path: Option<String>,
3140    /// How the API key was sourced
3141    #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
3142    pub api_key_source: Option<ApiKeySource>,
3143    /// Output style
3144    #[serde(skip_serializing_if = "Option::is_none")]
3145    pub output_style: Option<OutputStyle>,
3146    /// Permission mode
3147    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
3148    pub permission_mode: Option<InitPermissionMode>,
3149
3150    /// Message-level unique identifier
3151    #[serde(skip_serializing_if = "Option::is_none")]
3152    pub uuid: Option<String>,
3153
3154    /// Memory storage paths (e.g., {"auto": "/path/to/memory/"})
3155    #[serde(skip_serializing_if = "Option::is_none")]
3156    pub memory_paths: Option<MemoryPaths>,
3157
3158    /// Absolute path of this session's scratchpad directory, for clients
3159    /// that read files from the session. Omitted when the scratchpad is
3160    /// disabled, and from the redacted init sent over the Remote Control
3161    /// bridge and by the hosted worker (CLI 2.1.278+).
3162    #[serde(default, skip_serializing_if = "Option::is_none")]
3163    pub scratchpad_path: Option<String>,
3164
3165    /// Fast mode toggle state (e.g., "off")
3166    #[serde(skip_serializing_if = "Option::is_none")]
3167    pub fast_mode_state: Option<String>,
3168
3169    /// Why fast mode can't serve right now. Absent when nothing blocks it.
3170    #[serde(default, skip_serializing_if = "Option::is_none")]
3171    pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
3172
3173    /// MCP server config entries (from `--mcp-config`) that failed validation
3174    /// and were skipped. Affected servers are absent from `mcp_servers`.
3175    #[serde(default, skip_serializing_if = "Option::is_none")]
3176    pub mcp_server_errors: Option<Vec<McpServerError>>,
3177
3178    /// Whether analytics collection is disabled for this session.
3179    #[serde(default, skip_serializing_if = "Option::is_none")]
3180    pub analytics_disabled: Option<bool>,
3181
3182    /// Whether product-feedback prompts are disabled for this session.
3183    #[serde(default, skip_serializing_if = "Option::is_none")]
3184    pub product_feedback_disabled: Option<bool>,
3185
3186    /// API beta flags active for the session.
3187    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3188    pub betas: Vec<String>,
3189
3190    /// Open-set protocol capability names supported by this CLI.
3191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3192    pub capabilities: Vec<String>,
3193
3194    /// Plugin load errors.
3195    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3196    pub plugin_errors: Vec<PluginDiagnostic>,
3197
3198    /// Plugin load warnings.
3199    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3200    pub plugin_warnings: Vec<PluginDiagnostic>,
3201
3202    /// The effort level the session will send on its next request — after env
3203    /// overrides, session state, org caps, and model-support downgrades
3204    /// (`"low"` | `"medium"` | `"high"` | `"xhigh"` | `"max"`). `None` when no
3205    /// effort parameter will be sent, or on CLIs before 2.1.239.
3206    #[serde(default, skip_serializing_if = "Option::is_none")]
3207    pub effort: Option<String>,
3208
3209    /// Only on init frames written by the headless stream-json client of a
3210    /// cloud-hosted session: a per-frame snapshot of the cloud session's id,
3211    /// view URL, device binding, and directory-sync state. Absent in every
3212    /// other mode. Stored as raw JSON (the shape is internal and evolving).
3213    #[serde(default, skip_serializing_if = "Option::is_none")]
3214    pub cloud_session: Option<Value>,
3215
3216    /// The terminal's server-configured `◆ <text>` footer pill, carried so a
3217    /// host UI can render the same pill. Absent when nothing is configured
3218    /// (CLI 2.1.259+).
3219    #[serde(default, skip_serializing_if = "Option::is_none")]
3220    pub footer_indicator: Option<FooterIndicator>,
3221
3222    /// This cloud worker's life (`CLAUDE_CODE_WORKER_EPOCH`): a new number
3223    /// each time the session's worker is started. Absent outside cloud
3224    /// workers and on CLIs before 2.1.259.
3225    #[serde(default, skip_serializing_if = "Option::is_none")]
3226    pub worker_epoch: Option<u64>,
3227
3228    /// Windows only: the absolute path of the PowerShell binary this session
3229    /// runs PowerShell commands with, or `Some(None)` (wire `null`) when
3230    /// none was found. Absent on other platforms and on CLIs before 2.1.259.
3231    #[serde(default, skip_serializing_if = "Option::is_none")]
3232    pub powershell_path: Option<Option<String>>,
3233
3234    /// Cold-start telemetry for hosted (CCR) sessions: named startup phases
3235    /// and resume-hydration counters. Absent elsewhere. Stored as raw JSON
3236    /// (the shape is internal and evolving) (CLI 2.1.266+).
3237    #[serde(default, skip_serializing_if = "Option::is_none")]
3238    pub startup_timing: Option<serde_json::Map<String, Value>>,
3239
3240    /// Whether per-turn effort is active for this frame's `model`. `true`:
3241    /// an effort change keeps the prompt cache, because the effort is sent
3242    /// inside the conversation at the turn where it changes. `false`: the
3243    /// effort is sent only as the top-level parameter, so a change rewrites
3244    /// the cached prefix. Mid-session the CLI reports only a change to
3245    /// `false` (`system/per_turn_effort_changed`); `true` comes only from an
3246    /// init and is provisional after a fresh process or once the
3247    /// conversation's refusals are cleared, so keep listening all session.
3248    /// Absent means unknown (CLI 2.1.281+).
3249    #[serde(default, skip_serializing_if = "Option::is_none")]
3250    pub per_turn_effort_active: Option<bool>,
3251
3252    /// Whether typing `ultrathink` runs the turn at `xhigh` effort on this
3253    /// frame's `model` (and, when the session is already at `xhigh` or
3254    /// `max`, leaves the turn as it is); a host highlights the word only
3255    /// while this is `true`. After `system/per_turn_effort_changed`, treat it
3256    /// as `false` until the next init. The same answer as `get_settings`'
3257    /// `applied.ultrathinkAvailable`. Sent by the stream-json (Agent SDK)
3258    /// host; absent means unknown (CLI 2.1.316+).
3259    #[serde(default, skip_serializing_if = "Option::is_none")]
3260    pub ultrathink_available: Option<bool>,
3261
3262    /// `true` when a user message carrying
3263    /// [`UserMessage::ultrathink_typed`] that passes that field's checks runs
3264    /// its turn at `xhigh` effort on this frame's `model` (and, when the
3265    /// session is already at `xhigh` or `max`, leaves the turn as it is). A
3266    /// client colors the word and sends the statement only while this is
3267    /// `true`. After `system/per_turn_effort_changed`, treat it as `false`
3268    /// until the next init. Absent means unknown: color nothing and send no
3269    /// statement. Sent by the stream-json (Agent SDK) host and on Remote
3270    /// Control bridge init frames (CLI 2.1.317+).
3271    #[serde(default, skip_serializing_if = "Option::is_none")]
3272    pub ultrathink_typed_raises: Option<bool>,
3273
3274    /// Only on a Cowork cloud worker's init: the thinking this turn runs
3275    /// with. Absent when a request changed the session's thinking after the
3276    /// turn's start, or the session has no thinking setting of its own
3277    /// (CLI 2.1.317+).
3278    #[serde(default, skip_serializing_if = "Option::is_none")]
3279    pub client_thinking: Option<ClientThinking>,
3280
3281    /// Only on a Cowork cloud worker's init: `Some(true)` when an
3282    /// `apply_flag_settings` of exactly `{switchModelsOnFlag: true}` would add
3283    /// nothing now (the session holds the value, and no fast mode resync or
3284    /// auto mode check is due). Absent: send the request (CLI 2.1.317+).
3285    #[serde(default, skip_serializing_if = "Option::is_none")]
3286    pub flag_restate_noop: Option<bool>,
3287
3288    /// Absolute path of the one folder of synced skills whose files
3289    /// `read_file` serves in this session; a client may admit paths below it
3290    /// as it admits paths below [`scratchpad_path`](Self::scratchpad_path).
3291    /// Read only; a symbolic link, a dot-named part or a file at its top is
3292    /// refused. Sent by a self-hosted runner's session alone; omitted where
3293    /// the session serves no such folder, and wherever `original_cwd` is
3294    /// withheld (CLI 2.1.317+).
3295    #[serde(default, skip_serializing_if = "Option::is_none")]
3296    pub synced_skills_path: Option<String>,
3297
3298    /// Whether the session's transcript is in focus view. Toggled by
3299    /// `/focus`, including over Remote Control. Present on Remote Control
3300    /// bridge init frames and on the per-turn init of headless stream-json
3301    /// runs; absent on hosts that do not publish it and on CLIs before
3302    /// 2.1.281. Re-emitted inits carry the current value — the newest frame
3303    /// wins.
3304    #[serde(default, skip_serializing_if = "Option::is_none")]
3305    pub view_mode: Option<ViewMode>,
3306
3307    /// Absolute path of the folder the session's file permission checks are
3308    /// anchored at, for clients that read files from the session. Unlike
3309    /// `cwd` it does not follow the shell: it moves only on `/cd`, on
3310    /// entering or leaving a worktree, and on a session restore. Omitted on
3311    /// the redacted Remote Control / hosted-worker inits, in a session with
3312    /// no directory, for HIPAA organizations, and on CLIs before 2.1.301.
3313    #[serde(default, skip_serializing_if = "Option::is_none")]
3314    pub original_cwd: Option<String>,
3315
3316    /// Absolute paths of the folders added to the session (`--add-dir`, the
3317    /// SDK `additionalDirectories` option, settings, a grant, or a host's
3318    /// mid-session request), in no particular order; `Some(vec![])` when
3319    /// there are none. Folders added only by repository-delivered settings
3320    /// files are left out. Grants nothing: the CLI's own check decides every
3321    /// read. `None` means not told (not "none"): omitted on the same lanes as
3322    /// [`original_cwd`](Self::original_cwd), by hosts that have not wired it,
3323    /// and on CLIs before 2.1.301.
3324    #[serde(default, skip_serializing_if = "Option::is_none")]
3325    pub additional_directories: Option<Vec<String>>,
3326}
3327
3328/// [`InitMessage::client_thinking`]: the thinking a Cowork cloud worker's
3329/// turn runs with (CLI 2.1.317+).
3330#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3331pub struct ClientThinking {
3332    /// `0` with thinking off, the fixed budget, or `None` (wire `null`) with
3333    /// no fixed budget.
3334    #[serde(default)]
3335    pub max_thinking_tokens: Option<u64>,
3336    /// `summarized`, `omitted` or `highlights`; `None` (wire `null`) with
3337    /// none, with thinking off, and for a `highlights` this session cannot
3338    /// send.
3339    #[serde(default)]
3340    pub thinking_display: Option<String>,
3341}
3342
3343/// The server-configured session indicator that the terminal renders as a
3344/// `◆ <text>` pill in the prompt footer, carried on `system/init` and the
3345/// `initialize` response (CLI 2.1.259+).
3346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3347pub struct FooterIndicator {
3348    /// The label to show — already sanitized to a single line of plain text,
3349    /// exactly as the terminal footer renders it after its `◆` glyph.
3350    pub text: String,
3351}
3352
3353/// The session's transcript view, carried as [`InitMessage::view_mode`]
3354/// (CLI 2.1.281+).
3355#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3356pub enum ViewMode {
3357    /// The model is told the user sees only its final message per turn, so
3358    /// clients may collapse each turn to the prompt and the final response.
3359    Focus,
3360    /// The ordinary full transcript.
3361    Default,
3362    /// A mode not yet known to this version of the crate.
3363    Unknown(String),
3364}
3365
3366impl ViewMode {
3367    pub fn as_str(&self) -> &str {
3368        match self {
3369            Self::Focus => "focus",
3370            Self::Default => "default",
3371            Self::Unknown(s) => s.as_str(),
3372        }
3373    }
3374}
3375
3376impl fmt::Display for ViewMode {
3377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3378        f.write_str(self.as_str())
3379    }
3380}
3381
3382impl From<&str> for ViewMode {
3383    fn from(s: &str) -> Self {
3384        match s {
3385            "focus" => Self::Focus,
3386            "default" => Self::Default,
3387            other => Self::Unknown(other.to_string()),
3388        }
3389    }
3390}
3391
3392impl Serialize for ViewMode {
3393    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3394        serializer.serialize_str(self.as_str())
3395    }
3396}
3397
3398impl<'de> Deserialize<'de> for ViewMode {
3399    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3400        let s = String::deserialize(deserializer)?;
3401        Ok(Self::from(s.as_str()))
3402    }
3403}
3404
3405/// Status system message - sent during operations like context compaction
3406#[derive(Debug, Clone, Serialize, Deserialize)]
3407pub struct StatusMessage {
3408    /// Session identifier
3409    pub session_id: String,
3410    /// Current status (e.g., compacting) or null when complete
3411    pub status: Option<StatusMessageStatus>,
3412    /// Unique identifier for this message
3413    #[serde(skip_serializing_if = "Option::is_none")]
3414    pub uuid: Option<String>,
3415    /// Current permission mode when changed mid-session.
3416    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
3417    pub permission_mode: Option<InitPermissionMode>,
3418    #[serde(skip_serializing_if = "Option::is_none")]
3419    pub compact_result: Option<String>,
3420    #[serde(skip_serializing_if = "Option::is_none")]
3421    pub compact_error: Option<String>,
3422    /// On the `compacting` statuses of an automatic compaction (absent on a
3423    /// typed `/compact`): characters of the summary streamed so far (text
3424    /// and tool-input deltas, not thinking). Returns to 0 at each new attempt
3425    /// and stays 0 while a background summary is awaited. A rising count
3426    /// shows progress; it is not a share of the final length. Absent from
3427    /// CLIs before 2.1.295. Marked `@internal` upstream.
3428    #[serde(default, skip_serializing_if = "Option::is_none")]
3429    pub compact_output_chars: Option<u64>,
3430    /// Present, and true, once at start-up when
3431    /// `CLAUDE_CODE_RESUME_INTERRUPTED_TURN` asked for the interrupted turn
3432    /// to be re-run and this process did not re-run it: the turn ran in plan
3433    /// mode and the process came back in another mode, where the re-run
3434    /// could carry out a plan nobody approved. Marked `@internal` upstream
3435    /// (CLI 2.1.319+).
3436    #[serde(default, skip_serializing_if = "Option::is_none")]
3437    pub resume_held_for_plan: Option<bool>,
3438}
3439
3440/// Compact boundary message - marks where context compaction occurred
3441#[derive(Debug, Clone, Serialize, Deserialize)]
3442pub struct CompactBoundaryMessage {
3443    /// Session identifier
3444    pub session_id: String,
3445    /// Metadata about the compaction
3446    pub compact_metadata: CompactMetadata,
3447    /// Human-readable summary of what was compacted, when the CLI emits one.
3448    ///
3449    /// Also accepted under the `content` / `text` wire keys.
3450    #[serde(
3451        default,
3452        skip_serializing_if = "Option::is_none",
3453        alias = "content",
3454        alias = "text"
3455    )]
3456    pub summary: Option<String>,
3457    /// Number of messages summarized in this compaction pass, when present.
3458    ///
3459    /// Also accepted under the `message_count` wire key.
3460    #[serde(
3461        default,
3462        skip_serializing_if = "Option::is_none",
3463        alias = "message_count"
3464    )]
3465    pub leaf_message_count: Option<u32>,
3466    /// Wall-clock duration of the compaction pass in milliseconds, when present.
3467    #[serde(default, skip_serializing_if = "Option::is_none")]
3468    pub duration_ms: Option<u64>,
3469    /// Unique identifier for this message
3470    #[serde(skip_serializing_if = "Option::is_none")]
3471    pub uuid: Option<String>,
3472    /// Logical parent across the compaction boundary.
3473    #[serde(skip_serializing_if = "Option::is_none")]
3474    pub logical_parent_uuid: Option<Option<String>>,
3475    /// Replayed history rather than a live message, stamped by the Remote
3476    /// Control bridge when it flushes history to the session server
3477    /// (CLI 2.1.266+).
3478    #[serde(default, skip_serializing_if = "Option::is_none")]
3479    pub historical: Option<bool>,
3480}
3481
3482/// Metadata about context compaction
3483#[derive(Debug, Clone, Serialize, Deserialize)]
3484pub struct CompactMetadata {
3485    /// Number of tokens before compaction
3486    pub pre_tokens: u64,
3487    /// What triggered the compaction
3488    pub trigger: CompactionTrigger,
3489    #[serde(default, skip_serializing_if = "Option::is_none")]
3490    pub post_tokens: Option<u64>,
3491    #[serde(default, skip_serializing_if = "Option::is_none")]
3492    pub cumulative_dropped_tokens: Option<u64>,
3493    #[serde(default, skip_serializing_if = "Option::is_none")]
3494    pub duration_ms: Option<u64>,
3495    #[serde(default, skip_serializing_if = "Option::is_none")]
3496    pub user_context: Option<String>,
3497    #[serde(default, skip_serializing_if = "Option::is_none")]
3498    pub messages_summarized: Option<u64>,
3499    #[serde(default, skip_serializing_if = "Option::is_none")]
3500    pub precomputed: Option<bool>,
3501    #[serde(default, skip_serializing_if = "Option::is_none")]
3502    pub pre_compact_discovered_tools: Option<Vec<String>>,
3503    /// The artifact versions the conversation held before this compaction,
3504    /// most recently changed first; empty when it held none. A resumed
3505    /// session replays these so it can update those artifacts without
3506    /// reading them again (CLI 2.1.285+).
3507    #[serde(default, skip_serializing_if = "Option::is_none")]
3508    pub pre_compact_artifact_read_versions: Option<Vec<ArtifactReadVersion>>,
3509    #[serde(default, skip_serializing_if = "Option::is_none")]
3510    pub preserved_segment: Option<PreservedSegment>,
3511    #[serde(default, skip_serializing_if = "Option::is_none")]
3512    pub preserved_messages: Option<PreservedMessages>,
3513}
3514
3515/// One artifact version a conversation held before a compaction, carried in
3516/// [`CompactMetadata::pre_compact_artifact_read_versions`].
3517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3518pub struct ArtifactReadVersion {
3519    /// The artifact id.
3520    pub slug: String,
3521    /// The version read or published.
3522    pub ver: String,
3523}
3524
3525#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3526pub struct PreservedSegment {
3527    pub head_uuid: String,
3528    pub anchor_uuid: String,
3529    pub tail_uuid: String,
3530}
3531
3532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3533pub struct PreservedMessages {
3534    pub anchor_uuid: String,
3535    pub uuids: Vec<String>,
3536    #[serde(default, skip_serializing_if = "Option::is_none")]
3537    pub all_uuids: Option<Vec<String>>,
3538}
3539
3540// ---------------------------------------------------------------------------
3541// Task system message types (task_started, task_progress, task_notification)
3542// ---------------------------------------------------------------------------
3543
3544/// Cumulative usage statistics for a background task.
3545#[derive(Debug, Clone, Serialize, Deserialize)]
3546pub struct TaskUsage {
3547    /// Wall-clock milliseconds since the task started.
3548    pub duration_ms: u64,
3549    /// Total number of tool calls made so far.
3550    pub tool_uses: u64,
3551    /// Total tokens consumed so far.
3552    pub total_tokens: u64,
3553}
3554
3555/// The kind of background task.
3556#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3557pub enum TaskType {
3558    /// A sub-agent task (e.g., Explore, Plan).
3559    LocalAgent,
3560    /// A background bash command.
3561    LocalBash,
3562    /// A local workflow task.
3563    LocalWorkflow,
3564    /// A task type not yet known to this version of the crate.
3565    Unknown(String),
3566}
3567
3568impl TaskType {
3569    pub fn as_str(&self) -> &str {
3570        match self {
3571            Self::LocalAgent => "local_agent",
3572            Self::LocalBash => "local_bash",
3573            Self::LocalWorkflow => "local_workflow",
3574            Self::Unknown(s) => s.as_str(),
3575        }
3576    }
3577}
3578
3579impl fmt::Display for TaskType {
3580    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3581        f.write_str(self.as_str())
3582    }
3583}
3584
3585impl From<&str> for TaskType {
3586    fn from(s: &str) -> Self {
3587        match s {
3588            "local_agent" => Self::LocalAgent,
3589            "local_bash" => Self::LocalBash,
3590            "local_workflow" => Self::LocalWorkflow,
3591            other => Self::Unknown(other.to_string()),
3592        }
3593    }
3594}
3595
3596impl Serialize for TaskType {
3597    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3598        serializer.serialize_str(self.as_str())
3599    }
3600}
3601
3602impl<'de> Deserialize<'de> for TaskType {
3603    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3604        let s = String::deserialize(deserializer)?;
3605        Ok(Self::from(s.as_str()))
3606    }
3607}
3608
3609/// Completion status of a background task.
3610#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3611pub enum TaskStatus {
3612    Pending,
3613    Running,
3614    Completed,
3615    Failed,
3616    Killed,
3617    Paused,
3618    Stopped,
3619    Unknown(String),
3620}
3621
3622impl TaskStatus {
3623    pub fn as_str(&self) -> &str {
3624        match self {
3625            Self::Pending => "pending",
3626            Self::Running => "running",
3627            Self::Completed => "completed",
3628            Self::Failed => "failed",
3629            Self::Killed => "killed",
3630            Self::Paused => "paused",
3631            Self::Stopped => "stopped",
3632            Self::Unknown(s) => s.as_str(),
3633        }
3634    }
3635}
3636
3637impl fmt::Display for TaskStatus {
3638    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3639        f.write_str(self.as_str())
3640    }
3641}
3642
3643impl From<&str> for TaskStatus {
3644    fn from(s: &str) -> Self {
3645        match s {
3646            "pending" => Self::Pending,
3647            "running" => Self::Running,
3648            "completed" => Self::Completed,
3649            "failed" => Self::Failed,
3650            "killed" => Self::Killed,
3651            "paused" => Self::Paused,
3652            "stopped" => Self::Stopped,
3653            other => Self::Unknown(other.to_string()),
3654        }
3655    }
3656}
3657
3658impl Serialize for TaskStatus {
3659    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3660        serializer.serialize_str(self.as_str())
3661    }
3662}
3663
3664impl<'de> Deserialize<'de> for TaskStatus {
3665    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3666        let s = String::deserialize(deserializer)?;
3667        Ok(Self::from(s.as_str()))
3668    }
3669}
3670
3671/// `task_started` system message — emitted once when a background task begins.
3672#[derive(Debug, Clone, Serialize, Deserialize)]
3673pub struct TaskStartedMessage {
3674    pub session_id: String,
3675    pub task_id: String,
3676    /// Id of one run of the task: equal on every `task_*` event and
3677    /// `background_tasks_changed` entry of that run. A resumed task keeps its
3678    /// `task_id` and gets a new `run_id`; the run ids of one task sort, by
3679    /// plain string comparison, in the order the runs opened. Absent for a
3680    /// task this process did not register (CLI 2.1.292+).
3681    #[serde(default, skip_serializing_if = "Option::is_none")]
3682    pub run_id: Option<String>,
3683    /// `task_id` of the subagent (`local_agent`) task whose agent launched
3684    /// this task. Absent when the launcher is the main thread, has no
3685    /// `task_id` of its own, or is no longer tracked. The parent may be a
3686    /// foreground or already-ended task, so treat an unknown id as no parent
3687    /// (CLI 2.1.292+).
3688    #[serde(default, skip_serializing_if = "Option::is_none")]
3689    pub parent_task_id: Option<String>,
3690    #[serde(default, skip_serializing_if = "Option::is_none")]
3691    pub task_type: Option<TaskType>,
3692    #[serde(default, skip_serializing_if = "Option::is_none")]
3693    pub tool_use_id: Option<String>,
3694    pub description: String,
3695    /// The subagent type for `local_agent` tasks (e.g. `general-purpose`,
3696    /// `Explore`). Absent for `local_bash` tasks.
3697    #[serde(default, skip_serializing_if = "Option::is_none")]
3698    pub subagent_type: Option<String>,
3699    /// Whether the task was registered in the background (`true`) or in the
3700    /// foreground with the spawning tool call blocking on it (`false`). A
3701    /// later move to the background arrives as `task_updated`
3702    /// `patch.is_backgrounded`. Set for `local_agent` and `local_bash` tasks
3703    /// (CLI 2.1.239+).
3704    #[serde(default, skip_serializing_if = "Option::is_none")]
3705    pub is_backgrounded: Option<bool>,
3706    /// True for a run of a resumed subagent (`local_agent`) that the tool
3707    /// call which resumed it waits for, as `SendMessage` does when
3708    /// background tasks are disabled; [`tool_use_id`](Self::tool_use_id)
3709    /// then names that call. The run has ended by the time that call
3710    /// returns, and its report goes into that call's result, so expect no
3711    /// follow-up turn when it ends. A later resume that nothing waits for
3712    /// starts a new run without the field (CLI 2.1.293+).
3713    #[serde(default, skip_serializing_if = "Option::is_none")]
3714    pub awaited: Option<bool>,
3715    /// Nesting depth of a spawned subagent (`local_agent`) task: 1 for a
3716    /// top-level spawn, N+1 when spawned from inside a depth-N agent. Not set
3717    /// on other tasks (CLI 2.1.239+).
3718    #[serde(default, skip_serializing_if = "Option::is_none")]
3719    pub spawn_depth: Option<u32>,
3720    /// The prompt handed to the subagent. Present for `local_agent` tasks.
3721    #[serde(default, skip_serializing_if = "Option::is_none")]
3722    pub prompt: Option<String>,
3723    #[serde(default, skip_serializing_if = "Option::is_none")]
3724    pub workflow_name: Option<String>,
3725    #[serde(default, skip_serializing_if = "Option::is_none")]
3726    pub skip_transcript: Option<bool>,
3727    /// True for housekeeping tasks the CLI does not surface as user work
3728    /// (every `skip_transcript` task, plus auto-started live-update
3729    /// watchers); hosts should exclude them from activity indicators
3730    /// (CLI 2.1.259+).
3731    #[serde(default, skip_serializing_if = "Option::is_none")]
3732    pub ambient: Option<bool>,
3733    pub uuid: String,
3734}
3735
3736/// `task_updated` system message — emitted when a background task's state
3737/// changes (e.g. transitions to `completed`). Carries a partial `patch` of the
3738/// fields that changed rather than the full task record.
3739#[derive(Debug, Clone, Serialize, Deserialize)]
3740pub struct TaskUpdatedMessage {
3741    pub session_id: String,
3742    pub task_id: String,
3743    /// Id of the task run this update belongs to; see
3744    /// [`TaskStartedMessage::run_id`] (CLI 2.1.292+).
3745    #[serde(default, skip_serializing_if = "Option::is_none")]
3746    pub run_id: Option<String>,
3747    pub patch: TaskPatch,
3748    pub uuid: String,
3749}
3750
3751/// The partial update carried by a [`TaskUpdatedMessage`]. Every field is
3752/// optional because the CLI only sends the keys that changed.
3753#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3754pub struct TaskPatch {
3755    #[serde(default, skip_serializing_if = "Option::is_none")]
3756    pub status: Option<TaskStatus>,
3757    /// Wall-clock epoch milliseconds when the task finished, when the patch
3758    /// reports completion.
3759    #[serde(default, skip_serializing_if = "Option::is_none")]
3760    pub end_time: Option<u64>,
3761    #[serde(default, skip_serializing_if = "Option::is_none")]
3762    pub description: Option<String>,
3763    #[serde(default, skip_serializing_if = "Option::is_none")]
3764    pub total_paused_ms: Option<u64>,
3765    #[serde(default, skip_serializing_if = "Option::is_none")]
3766    pub error: Option<String>,
3767    #[serde(default, skip_serializing_if = "Option::is_none")]
3768    pub is_backgrounded: Option<bool>,
3769}
3770
3771/// `thinking_tokens` system message — emitted as the model streams extended
3772/// thinking, reporting the running estimate of thinking tokens consumed.
3773#[derive(Debug, Clone, Serialize, Deserialize)]
3774pub struct ThinkingTokensMessage {
3775    pub session_id: String,
3776    /// Running estimate of total thinking tokens for the current turn.
3777    pub estimated_tokens: u64,
3778    /// Increase in the estimate since the previous `thinking_tokens` event.
3779    pub estimated_tokens_delta: u64,
3780    /// Client uuid of the user message that triggered this turn, stamped on
3781    /// every `thinking_tokens` frame of a headless turn so a consumer can
3782    /// attribute thinking progress to the send it answers before any reply
3783    /// frame arrives. Absent on synthetic/scheduled (meta) turns, on turns
3784    /// without a client uuid, on Remote Control sessions, and from CLIs
3785    /// before 2.1.261.
3786    #[serde(default, skip_serializing_if = "Option::is_none")]
3787    pub user_message_uuid: Option<String>,
3788    pub uuid: String,
3789}
3790
3791/// `task_progress` system message — emitted periodically as a background
3792/// agent task executes tools. Not emitted for `local_bash` tasks.
3793#[derive(Debug, Clone, Serialize, Deserialize)]
3794pub struct TaskProgressMessage {
3795    pub session_id: String,
3796    pub task_id: String,
3797    /// Id of the task run this progress belongs to; see
3798    /// [`TaskStartedMessage::run_id`] (CLI 2.1.292+).
3799    #[serde(default, skip_serializing_if = "Option::is_none")]
3800    pub run_id: Option<String>,
3801    #[serde(default, skip_serializing_if = "Option::is_none")]
3802    pub tool_use_id: Option<String>,
3803    pub description: String,
3804    #[serde(default, skip_serializing_if = "Option::is_none")]
3805    pub last_tool_name: Option<String>,
3806    pub usage: TaskUsage,
3807    /// Subagent type for `local_agent` tasks (e.g. `Explore`).
3808    #[serde(default, skip_serializing_if = "Option::is_none")]
3809    pub subagent_type: Option<String>,
3810    #[serde(default, skip_serializing_if = "Option::is_none")]
3811    pub summary: Option<String>,
3812    pub uuid: String,
3813}
3814
3815/// `task_notification` system message — emitted once when a background
3816/// task completes or fails.
3817#[derive(Debug, Clone, Serialize, Deserialize)]
3818pub struct TaskNotificationMessage {
3819    pub session_id: String,
3820    pub task_id: String,
3821    /// Id of the task run this notification closes; see
3822    /// [`TaskStartedMessage::run_id`] (CLI 2.1.292+).
3823    #[serde(default, skip_serializing_if = "Option::is_none")]
3824    pub run_id: Option<String>,
3825    pub status: TaskStatus,
3826    /// Machine-readable cause, set only when the task did not end through an
3827    /// ordinary completion, failure, or stop (CLI 2.1.273+).
3828    #[serde(default, skip_serializing_if = "Option::is_none")]
3829    pub reason: Option<TaskEndReason>,
3830    pub summary: String,
3831    pub output_file: Option<String>,
3832    #[serde(skip_serializing_if = "Option::is_none")]
3833    pub tool_use_id: Option<String>,
3834    #[serde(skip_serializing_if = "Option::is_none")]
3835    pub usage: Option<TaskUsage>,
3836    /// For a backgrounded MCP task that completed, the `resource_link`
3837    /// content blocks of its final result — the files it returned by
3838    /// reference — collected from the raw result before the CLI renders it
3839    /// as text. Join to the originating call via `tool_use_id`. Absent when
3840    /// the result had none or the task is any other type (CLI 2.1.259+).
3841    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3842    pub resource_links: Vec<ResourceLink>,
3843    /// On a `completed` notification of a subagent that reports through the
3844    /// SubagentHandback tool: how its report reached its caller. Left out
3845    /// when the subagent failed, was stopped, or is still waiting on work;
3846    /// `summary` is then not a report. When present, `summary` holds harness
3847    /// notes written for the model. A later notification for the same
3848    /// `task_id` replaces what an earlier one showed (CLI 2.1.292+).
3849    #[serde(default, skip_serializing_if = "Option::is_none")]
3850    pub handback: Option<TaskHandback>,
3851    /// The report a `send` or `flagged` hand-back delivered: when present,
3852    /// render it instead of `summary`, with its `warning`, if any, above its
3853    /// `text`. Absent for `withheld` (CLI 2.1.292+).
3854    #[serde(default, skip_serializing_if = "Option::is_none")]
3855    pub handback_report: Option<HandbackReport>,
3856    #[serde(default, skip_serializing_if = "Option::is_none")]
3857    pub skip_transcript: Option<bool>,
3858    /// True for housekeeping tasks the CLI does not surface as user work;
3859    /// hosts should exclude them from activity indicators (CLI 2.1.259+).
3860    #[serde(default, skip_serializing_if = "Option::is_none")]
3861    pub ambient: Option<bool>,
3862    #[serde(skip_serializing_if = "Option::is_none")]
3863    pub uuid: Option<String>,
3864}
3865
3866/// How a subagent's hand-back report reached its caller
3867/// ([`TaskNotificationMessage::handback`]).
3868#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3869pub enum TaskHandback {
3870    /// The report was delivered.
3871    Send,
3872    /// The report was delivered with an auto-mode warning.
3873    Flagged,
3874    /// The report was held back; no `handback_report` is sent.
3875    Withheld,
3876    /// A value not yet known to this version of the crate.
3877    Unknown(String),
3878}
3879
3880impl TaskHandback {
3881    pub fn as_str(&self) -> &str {
3882        match self {
3883            Self::Send => "send",
3884            Self::Flagged => "flagged",
3885            Self::Withheld => "withheld",
3886            Self::Unknown(s) => s.as_str(),
3887        }
3888    }
3889}
3890
3891impl fmt::Display for TaskHandback {
3892    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3893        f.write_str(self.as_str())
3894    }
3895}
3896
3897impl From<&str> for TaskHandback {
3898    fn from(s: &str) -> Self {
3899        match s {
3900            "send" => Self::Send,
3901            "flagged" => Self::Flagged,
3902            "withheld" => Self::Withheld,
3903            other => Self::Unknown(other.to_string()),
3904        }
3905    }
3906}
3907
3908impl Serialize for TaskHandback {
3909    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3910        serializer.serialize_str(self.as_str())
3911    }
3912}
3913
3914impl<'de> Deserialize<'de> for TaskHandback {
3915    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3916        let s = String::deserialize(deserializer)?;
3917        Ok(Self::from(s.as_str()))
3918    }
3919}
3920
3921/// A subagent's delivered hand-back report
3922/// ([`TaskNotificationMessage::handback_report`]).
3923#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3924pub struct HandbackReport {
3925    /// The subagent's whole report, never truncated, with a backslash
3926    /// inserted into text that imitates harness markup.
3927    pub text: String,
3928    /// Auto mode's warning to show above `text`: a security warning, or a
3929    /// note that the review could not run.
3930    #[serde(default, skip_serializing_if = "Option::is_none")]
3931    pub warning: Option<String>,
3932}
3933
3934/// API error category attached to assistant wrapper frames.
3935#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3936pub enum AssistantErrorKind {
3937    AuthenticationFailed,
3938    OauthOrgNotAllowed,
3939    AccountOnHold,
3940    BillingError,
3941    RateLimit,
3942    Overloaded,
3943    InvalidRequest,
3944    ModelNotFound,
3945    ServerError,
3946    UnknownError,
3947    MaxOutputTokens,
3948    Unknown(String),
3949}
3950
3951impl AssistantErrorKind {
3952    pub fn as_str(&self) -> &str {
3953        match self {
3954            Self::AuthenticationFailed => "authentication_failed",
3955            Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
3956            Self::AccountOnHold => "account_on_hold",
3957            Self::BillingError => "billing_error",
3958            Self::RateLimit => "rate_limit",
3959            Self::Overloaded => "overloaded",
3960            Self::InvalidRequest => "invalid_request",
3961            Self::ModelNotFound => "model_not_found",
3962            Self::ServerError => "server_error",
3963            Self::UnknownError => "unknown",
3964            Self::MaxOutputTokens => "max_output_tokens",
3965            Self::Unknown(s) => s.as_str(),
3966        }
3967    }
3968}
3969
3970impl fmt::Display for AssistantErrorKind {
3971    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3972        f.write_str(self.as_str())
3973    }
3974}
3975
3976impl From<&str> for AssistantErrorKind {
3977    fn from(s: &str) -> Self {
3978        match s {
3979            "authentication_failed" => Self::AuthenticationFailed,
3980            "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
3981            "account_on_hold" => Self::AccountOnHold,
3982            "billing_error" => Self::BillingError,
3983            "rate_limit" => Self::RateLimit,
3984            "overloaded" => Self::Overloaded,
3985            "invalid_request" => Self::InvalidRequest,
3986            "model_not_found" => Self::ModelNotFound,
3987            "server_error" => Self::ServerError,
3988            "unknown" => Self::UnknownError,
3989            "max_output_tokens" => Self::MaxOutputTokens,
3990            other => Self::Unknown(other.to_string()),
3991        }
3992    }
3993}
3994
3995impl Serialize for AssistantErrorKind {
3996    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3997        serializer.serialize_str(self.as_str())
3998    }
3999}
4000
4001impl<'de> Deserialize<'de> for AssistantErrorKind {
4002    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4003        let s = String::deserialize(deserializer)?;
4004        Ok(Self::from(s.as_str()))
4005    }
4006}
4007
4008/// `code_change_published` system message — the session is now associated
4009/// with a published code change (a pull/merge request). Fires on creation and
4010/// whenever the session contributes to an existing one, so bind on every
4011/// event; re-emission for the same URL is possible and idempotent. Values are
4012/// scraped from captured command output — treat them as a binding hint and
4013/// verify against the forge before routing authenticated requests.
4014#[derive(Debug, Clone, Serialize, Deserialize)]
4015pub struct CodeChangePublishedMessage {
4016    /// Forge classification derived from the URL's shape (`github`,
4017    /// `github-enterprise`, `gitlab`, `bitbucket`, `gerrit` today). Open set
4018    /// — treat an unknown value as a valid provider, never as an error.
4019    pub provider: String,
4020    /// Web URL of the pull/merge request. Unverified.
4021    pub url: String,
4022    /// Repository path from the URL (`owner/name` on GitHub; may carry more
4023    /// segments on GitLab).
4024    pub repo: String,
4025    /// Provider-native change identifier — the PR/MR number as a string.
4026    pub identifier: String,
4027    /// What the session did that produced this announcement: the flag-aware
4028    /// `gh pr` verb it ran (`"created"`, `"edited"`, `"merged"`,
4029    /// `"commented"`, `"closed"`, `"reopened"`, `"ready"`, `"draft"`,
4030    /// `"auto-merge-enabled"`, `"auto-merge-disabled"`), `"pushed"` for a
4031    /// push to a branch that has a PR, `"checked-out"` for `gh pr checkout`,
4032    /// or `"started"` for the open change on the branch a Claude Desktop
4033    /// session began on. Always sent by current producers (CLI 2.1.239+),
4034    /// absent only from older ones. Open set — treat unknown values as valid.
4035    #[serde(default, skip_serializing_if = "Option::is_none")]
4036    pub action: Option<String>,
4037    /// The session's working branch when it produced the change. Sent for
4038    /// providers whose changes have no head branch of their own (`gerrit`),
4039    /// so a host can place the change on that checkout, and with `created`
4040    /// on any provider: the branch the create was opened from (its `--head`
4041    /// / `--source-branch` flag, else the working branch), so a host can show
4042    /// the new change before its own forge lookup answers (CLI 2.1.273+).
4043    /// Absent otherwise (CLI 2.1.259+).
4044    #[serde(default, skip_serializing_if = "Option::is_none")]
4045    pub branch: Option<String>,
4046    pub uuid: String,
4047    pub session_id: String,
4048}
4049
4050/// `vcs_state_changed` system message — a harness-observed shell command
4051/// mutated repository state. A cache-invalidation signal, deliberately
4052/// payload-free beyond classification: consumers re-read state (branch, head,
4053/// PR status) instead of decoding the event.
4054#[derive(Debug, Clone, Serialize, Deserialize)]
4055pub struct VcsStateChangedMessage {
4056    /// What class of mutation was observed. New kinds may be added — treat an
4057    /// unrecognized kind exactly like a recognized one (something changed).
4058    pub kind: VcsMutationKind,
4059    /// The session's working directory — a hint, not necessarily the mutated
4060    /// repo's path (`git -C` or an inner `cd` mutates elsewhere).
4061    pub cwd: String,
4062    /// The branch a commit landed on or a push updated. Commit and push
4063    /// events carry it; a command that pushed several branches emits one push
4064    /// event per branch. A best-effort hint: absent whenever attribution is
4065    /// uncertain, and never a required key (CLI 2.1.239+).
4066    #[serde(default, skip_serializing_if = "Option::is_none")]
4067    pub branch: Option<String>,
4068    pub uuid: String,
4069    pub session_id: String,
4070}
4071
4072/// Mutation class carried by a [`VcsStateChangedMessage`].
4073#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4074pub enum VcsMutationKind {
4075    Commit,
4076    Push,
4077    Merge,
4078    Rebase,
4079    /// A kind not yet known to this version of the crate.
4080    Unknown(String),
4081}
4082
4083impl VcsMutationKind {
4084    pub fn as_str(&self) -> &str {
4085        match self {
4086            Self::Commit => "commit",
4087            Self::Push => "push",
4088            Self::Merge => "merge",
4089            Self::Rebase => "rebase",
4090            Self::Unknown(s) => s.as_str(),
4091        }
4092    }
4093}
4094
4095impl fmt::Display for VcsMutationKind {
4096    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4097        f.write_str(self.as_str())
4098    }
4099}
4100
4101impl From<&str> for VcsMutationKind {
4102    fn from(s: &str) -> Self {
4103        match s {
4104            "commit" => Self::Commit,
4105            "push" => Self::Push,
4106            "merge" => Self::Merge,
4107            "rebase" => Self::Rebase,
4108            other => Self::Unknown(other.to_string()),
4109        }
4110    }
4111}
4112
4113impl Serialize for VcsMutationKind {
4114    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4115        serializer.serialize_str(self.as_str())
4116    }
4117}
4118
4119impl<'de> Deserialize<'de> for VcsMutationKind {
4120    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4121        let s = String::deserialize(deserializer)?;
4122        Ok(Self::from(s.as_str()))
4123    }
4124}
4125
4126/// `system/feedback_draft_queued` — a feedback draft was queued for submission.
4127#[derive(Debug, Clone, Serialize, Deserialize)]
4128pub struct FeedbackDraftQueuedMessage {
4129    pub draft_id: String,
4130    pub draft_type: String,
4131    pub title: String,
4132    pub details_preview: String,
4133    #[serde(default, skip_serializing_if = "Option::is_none")]
4134    pub uuid: Option<String>,
4135    #[serde(default, skip_serializing_if = "Option::is_none")]
4136    pub session_id: Option<String>,
4137    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4138    pub extra: serde_json::Map<String, Value>,
4139}
4140
4141/// `system/cloud_session_delta` — written only by the headless stream-json
4142/// client of a cloud-hosted session (the same client that puts
4143/// `cloud_session` on its init frames): the session's status changed between
4144/// two inits. Only after the first init; at most a few per second; none when
4145/// nothing differs. An init is always a complete snapshot, so a host that
4146/// (re)attaches resynchronises from the first init it reads and applies
4147/// these on top. Display-only (CLI 2.1.260+).
4148#[derive(Debug, Clone, Serialize, Deserialize)]
4149pub struct CloudSessionDeltaMessage {
4150    /// Rises by one with each of these frames this client writes (1 for the
4151    /// first); never reset by an init. A reader keeps the highest it has
4152    /// applied and drops a lower one.
4153    pub seq: u64,
4154    /// The top-level keys of `cloud_session` whose value differs from the
4155    /// last block this client wrote, e.g. `["serving"]`; never empty. A hint
4156    /// for what to redraw — the block is complete either way.
4157    pub changed: Vec<String>,
4158    /// The whole block exactly as the next init would carry it (see
4159    /// [`InitMessage::cloud_session`]): replace the held copy, do not merge.
4160    /// Stored as raw JSON (the shape is internal and evolving).
4161    pub cloud_session: Value,
4162    pub uuid: String,
4163    pub session_id: String,
4164    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4165    pub extra: serde_json::Map<String, Value>,
4166}
4167
4168/// `system/dev_intent` — the conversation, or the git repository it runs in,
4169/// shows a known kind of development work, for hosts that key tooling on it
4170/// (Claude Code Desktop opens its iOS Simulator entry point on `ios_app`).
4171/// Sent with `trigger` as its only other payload. Per conversation per
4172/// process, each kind is sent at most once from conversation evidence and at
4173/// most once from the project scan: conversation evidence when it first
4174/// completes, or at startup when a resumed conversation already has it;
4175/// project evidence (print-mode CLIs only, which is how the Agent SDK starts
4176/// it) at startup, after each `conversation_reset`, and at the first message
4177/// of a conversation whose earlier scan did not find every kind. Either can
4178/// arrive before `system/init`. A rewind or compaction never retracts it and
4179/// only a `conversation_reset` starts over, so treat each kind as a sticky
4180/// fact about the conversation: a client that needs only the kind can ignore
4181/// repeats, and one that needs to know what set it off reads `trigger`
4182/// (CLI 2.1.266+; `trigger` and the project scan from 2.1.273).
4183#[derive(Debug, Clone, Serialize, Deserialize)]
4184pub struct DevIntentMessage {
4185    /// What kind of development the evidence shows.
4186    pub kind: DevIntentKind,
4187    /// The evidence the detection fired on, for a host's own analytics. From
4188    /// the conversation it is the first platform-specific evidence seen;
4189    /// [`DevIntentTrigger::ProjectScan`] marks a scan of the session's git
4190    /// repository. Absent from CLIs before 2.1.273, which send only
4191    /// conversation evidence.
4192    #[serde(default, skip_serializing_if = "Option::is_none")]
4193    pub trigger: Option<DevIntentTrigger>,
4194    pub uuid: String,
4195    pub session_id: String,
4196    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4197    pub extra: serde_json::Map<String, Value>,
4198}
4199
4200/// The kind of development a [`DevIntentMessage`] reports. Open set: the CLI
4201/// says "more kinds will be added; ignore a kind you do not recognize", so
4202/// unrecognized values deserialize to [`DevIntentKind::Unknown`].
4203#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4204pub enum DevIntentKind {
4205    /// From the conversation: Claude wrote or edited a `.swift` file and
4206    /// something it wrote, read, or ran is iOS-specific (a macOS-only app or
4207    /// server-side Swift package never qualifies). From the project scan: the
4208    /// session's git repository has an Xcode project whose build settings
4209    /// name an iOS SDK or target iPhone or iPad.
4210    IosApp,
4211    /// From the conversation: Claude wrote or edited a `.kt`, `.kts` or
4212    /// `.java` file and something it wrote, read, or ran is Android-specific
4213    /// (a Kotlin server or a multiplatform module with no Android target
4214    /// never qualifies). From the project scan: the repository has an
4215    /// `AndroidManifest.xml` or the Android Gradle plugin in a build script
4216    /// or version catalog (CLI 2.1.273+).
4217    AndroidApp,
4218    /// A kind not yet known to this version of the crate.
4219    Unknown(String),
4220}
4221
4222impl DevIntentKind {
4223    pub fn as_str(&self) -> &str {
4224        match self {
4225            Self::IosApp => "ios_app",
4226            Self::AndroidApp => "android_app",
4227            Self::Unknown(s) => s.as_str(),
4228        }
4229    }
4230}
4231
4232impl fmt::Display for DevIntentKind {
4233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4234        f.write_str(self.as_str())
4235    }
4236}
4237
4238impl From<&str> for DevIntentKind {
4239    fn from(s: &str) -> Self {
4240        match s {
4241            "ios_app" => Self::IosApp,
4242            "android_app" => Self::AndroidApp,
4243            other => Self::Unknown(other.to_string()),
4244        }
4245    }
4246}
4247
4248impl Serialize for DevIntentKind {
4249    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4250        serializer.serialize_str(self.as_str())
4251    }
4252}
4253
4254impl<'de> Deserialize<'de> for DevIntentKind {
4255    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4256        let s = String::deserialize(deserializer)?;
4257        Ok(Self::from(s.as_str()))
4258    }
4259}
4260
4261/// The evidence a [`DevIntentMessage`] detection fired on. Open set: the CLI
4262/// says "more values will be added; ignore one you do not recognize", so
4263/// unrecognized values deserialize to [`DevIntentTrigger::Unknown`].
4264#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4265pub enum DevIntentTrigger {
4266    /// Reserved for rules that need no other evidence; no rule sends it yet.
4267    SwiftEdit,
4268    /// `import UIKit` or `.iOS(` in text Claude wrote.
4269    UikitImport,
4270    /// iOS build settings written, or seen in a tool result such as a
4271    /// pbxproj read.
4272    XcodeProject,
4273    /// `simctl`, an iOS SDK or a Simulator destination in a command Claude
4274    /// ran.
4275    IosCommand,
4276    /// Reserved for rules that need no other evidence; no rule sends it yet.
4277    KotlinEdit,
4278    /// Reserved for rules that need no other evidence; no rule sends it yet.
4279    JavaEdit,
4280    /// `import android.` in text Claude wrote.
4281    AndroidImport,
4282    /// A manifest path or body written, or seen in a tool result.
4283    AndroidManifest,
4284    /// The Android Gradle plugin written, or seen in a tool result.
4285    GradlePlugin,
4286    /// `adb`, the emulator, `sdkmanager`, `avdmanager`,
4287    /// `react-native run-android` or a Gradle variant task.
4288    AndroidCommand,
4289    /// For any kind: a scan of the session's git repository rather than
4290    /// conversation evidence.
4291    ProjectScan,
4292    /// A trigger not yet known to this version of the crate.
4293    Unknown(String),
4294}
4295
4296impl DevIntentTrigger {
4297    pub fn as_str(&self) -> &str {
4298        match self {
4299            Self::SwiftEdit => "swift_edit",
4300            Self::UikitImport => "uikit_import",
4301            Self::XcodeProject => "xcode_project",
4302            Self::IosCommand => "ios_command",
4303            Self::KotlinEdit => "kotlin_edit",
4304            Self::JavaEdit => "java_edit",
4305            Self::AndroidImport => "android_import",
4306            Self::AndroidManifest => "android_manifest",
4307            Self::GradlePlugin => "gradle_plugin",
4308            Self::AndroidCommand => "android_command",
4309            Self::ProjectScan => "project_scan",
4310            Self::Unknown(s) => s.as_str(),
4311        }
4312    }
4313}
4314
4315impl fmt::Display for DevIntentTrigger {
4316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4317        f.write_str(self.as_str())
4318    }
4319}
4320
4321impl From<&str> for DevIntentTrigger {
4322    fn from(s: &str) -> Self {
4323        match s {
4324            "swift_edit" => Self::SwiftEdit,
4325            "uikit_import" => Self::UikitImport,
4326            "xcode_project" => Self::XcodeProject,
4327            "ios_command" => Self::IosCommand,
4328            "kotlin_edit" => Self::KotlinEdit,
4329            "java_edit" => Self::JavaEdit,
4330            "android_import" => Self::AndroidImport,
4331            "android_manifest" => Self::AndroidManifest,
4332            "gradle_plugin" => Self::GradlePlugin,
4333            "android_command" => Self::AndroidCommand,
4334            "project_scan" => Self::ProjectScan,
4335            other => Self::Unknown(other.to_string()),
4336        }
4337    }
4338}
4339
4340impl Serialize for DevIntentTrigger {
4341    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4342        serializer.serialize_str(self.as_str())
4343    }
4344}
4345
4346impl<'de> Deserialize<'de> for DevIntentTrigger {
4347    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4348        let s = String::deserialize(deserializer)?;
4349        Ok(Self::from(s.as_str()))
4350    }
4351}
4352
4353/// Why a [`TaskNotificationMessage`] ended other than through an ordinary
4354/// completion, failure, or stop. Open set: unrecognized values deserialize
4355/// to [`TaskEndReason::Unknown`].
4356#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4357pub enum TaskEndReason {
4358    /// The worker process restarted and the resumed process found the task
4359    /// orphaned (always with status `stopped`).
4360    WorkerRestart,
4361    /// A reason not yet known to this version of the crate.
4362    Unknown(String),
4363}
4364
4365impl TaskEndReason {
4366    pub fn as_str(&self) -> &str {
4367        match self {
4368            Self::WorkerRestart => "worker_restart",
4369            Self::Unknown(s) => s.as_str(),
4370        }
4371    }
4372}
4373
4374impl fmt::Display for TaskEndReason {
4375    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4376        f.write_str(self.as_str())
4377    }
4378}
4379
4380impl From<&str> for TaskEndReason {
4381    fn from(s: &str) -> Self {
4382        match s {
4383            "worker_restart" => Self::WorkerRestart,
4384            other => Self::Unknown(other.to_string()),
4385        }
4386    }
4387}
4388
4389impl Serialize for TaskEndReason {
4390    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4391        serializer.serialize_str(self.as_str())
4392    }
4393}
4394
4395impl<'de> Deserialize<'de> for TaskEndReason {
4396    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4397        let s = String::deserialize(deserializer)?;
4398        Ok(Self::from(s.as_str()))
4399    }
4400}
4401
4402/// `system/turn_handoff_available` — emitted once by a cloud worker that
4403/// accepts the `turn_handoff` control request, right after it registers,
4404/// carrying what its registration wrote to `external_metadata.turn_handoff`.
4405/// Lets a session client learn the capability from the event stream instead
4406/// of reading worker state. Durable in the stream: a reader keeps the entry
4407/// with the newest `worker_epoch` it has seen and ignores older ones; a
4408/// worker life that does not accept `turn_handoff` emits nothing
4409/// (CLI 2.1.273+).
4410#[derive(Debug, Clone, Serialize, Deserialize)]
4411pub struct TurnHandoffAvailableMessage {
4412    /// Contract version of the handoff registration (currently `1`).
4413    pub v: u64,
4414    /// The tools whose calls this worker would accept.
4415    pub tools: Vec<String>,
4416    /// The worker life announcing it.
4417    pub worker_epoch: u64,
4418    /// Present, and true, only when this worker uses the `relay_marker`
4419    /// member of a `turn_handoff` request; a client sends that member to no
4420    /// other worker.
4421    #[serde(default, skip_serializing_if = "Option::is_none")]
4422    pub relay_marker: Option<bool>,
4423    /// Present, and true, when this worker starts a handed-off turn's calls
4424    /// only after every `stage_file` control request it received earlier has
4425    /// finished or failed, or a wait limit has passed; a worker that omits
4426    /// it may start them while files are still downloading (CLI 2.1.278+).
4427    /// Exception: a request with `filestore_path` set (a synced file) is not
4428    /// waited for, so a call that names that file may start before it is
4429    /// synced (CLI 2.1.287+).
4430    #[serde(default, skip_serializing_if = "Option::is_none")]
4431    pub staged_files: Option<bool>,
4432    /// True when user messages with `shouldQuery` false that reach this
4433    /// worker before a `turn_handoff` request are appended, and
4434    /// acknowledged, before the handed-off turn produces any output.
4435    /// Exception: a request that continues a turn whose leading lines an
4436    /// earlier request appended; that turn goes first. A worker that omits
4437    /// it may run the turn first (CLI 2.1.278+).
4438    #[serde(default, skip_serializing_if = "Option::is_none")]
4439    pub no_query_first: Option<bool>,
4440    /// Present, and true, only on a worker that takes a `stage_file` control
4441    /// request whose `mount_path` is `/uploads/.home/<path>` as a file for
4442    /// `/home/claude/<path>`: it writes the file there, answers ok with a
4443    /// noop (`already_present`, `path_occupied`, `refused_name`,
4444    /// `home_files_off`, `unchecked_announcement`, `resent_announcement`)
4445    /// and writes nothing, or answers an error. Any other worker treats that
4446    /// path as an ordinary upload (CLI 2.1.287+).
4447    #[serde(default, skip_serializing_if = "Option::is_none")]
4448    pub home_files: Option<bool>,
4449    /// Present, and true, only when this worker uses the `file_names` member
4450    /// of a `turn_handoff` request; a client sends that member to no other
4451    /// worker (CLI 2.1.287+).
4452    #[serde(default, skip_serializing_if = "Option::is_none")]
4453    pub file_names: Option<bool>,
4454    /// Present, and true, only on a worker that writes the files of the
4455    /// Write calls a `turn_handoff` request brings already answered, where
4456    /// their paths are under `/home/claude`. Before it appends a turn that
4457    /// has calls to run, it writes each such call's content to its path,
4458    /// replacing nothing. If it cannot write them all, it appends and runs
4459    /// nothing: the request is refused with an error that starts
4460    /// `invalid_handoff: carried_writes_`, or it is accepted and the turn
4461    /// ends in a result with `num_turns` 0. Any other worker writes no file
4462    /// for these calls (CLI 2.1.288+).
4463    #[serde(default, skip_serializing_if = "Option::is_none")]
4464    pub carried_writes: Option<bool>,
4465    /// Present, and true, only on a worker that rebuilt the conversation
4466    /// from the session's stored transcript before it read any request, and
4467    /// would not have started without it. A client may send such a worker
4468    /// again a `turn_handoff` request an earlier worker accepted, if the
4469    /// request names no calls (empty `tool_use_ids`), is not stopped, and its
4470    /// messages hold no tool call: the worker appends only the turn's
4471    /// messages its conversation lacks and, once the store confirms them,
4472    /// echoes the turn's last message back as an assistant event. The
4473    /// request can still be accepted or refused (`invalid_handoff:`) with no
4474    /// echo, e.g. after a rewind, a compaction or a Stop; an echo proves
4475    /// nothing if the same worker had the request before. The key promises
4476    /// nothing for a turn that holds a tool call. A worker without it may
4477    /// have started with none of the turn's messages (CLI 2.1.296+).
4478    #[serde(default, skip_serializing_if = "Option::is_none")]
4479    pub hydrates_carried_lines: Option<bool>,
4480    /// Present, and true, when this worker accepts a `turn_handoff` request
4481    /// in which an assistant message repeats the message id of the assistant
4482    /// message before it. A worker that omits it refuses such a request
4483    /// (`invalid_handoff`), so a client gives each message an id of its own
4484    /// there (CLI 2.1.300+).
4485    #[serde(default, skip_serializing_if = "Option::is_none")]
4486    pub shared_message_id: Option<bool>,
4487    /// Present, and true, only on a worker that reads the
4488    /// `requires_rewind_request_id` member of a `turn_handoff` request; a
4489    /// worker without this key ignores the member and appends the turn
4490    /// whether or not the rewind was made (CLI 2.1.311+).
4491    #[serde(default, skip_serializing_if = "Option::is_none")]
4492    pub requires_rewind_request_id: Option<bool>,
4493    pub uuid: String,
4494    pub session_id: String,
4495    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4496    pub extra: serde_json::Map<String, Value>,
4497}
4498
4499/// `system/turn_preempted` — the CLI itself stopped the running turn so a
4500/// user's rapid follow-up message is answered at once, exactly as a priority
4501/// `now` message would have (running shell commands are backgrounded, not
4502/// killed). Sent at the moment of the stop, so it precedes the stopped turn's
4503/// `result` frame (`terminal_reason` `aborted_streaming` or `aborted_tools`)
4504/// and its members' `cancelled` `command_lifecycle` frames: a host renders
4505/// that turn as superseded by the follow-up rather than as interrupted, does
4506/// not resend its messages, and treats only `preempted_by_uuid` as picked
4507/// up. At most one per burst. Emitted in `-p`/SDK sessions only, and only to
4508/// a consumer that declared `rapidFollowupPreempt` on its initialize request
4509/// while the feature's rollout flag is on (CLI 2.1.273+).
4510#[derive(Debug, Clone, Serialize, Deserialize)]
4511pub struct TurnPreemptedMessage {
4512    /// Why the turn was stopped. `rapid_followup`: the user's next message
4513    /// arrived before the running turn showed any output. More reasons may
4514    /// be added; treat an unknown one the same way.
4515    pub reason: String,
4516    /// The client-supplied uuid of the queued user message the turn was
4517    /// stopped for. It runs next, together with any messages queued behind
4518    /// it.
4519    pub preempted_by_uuid: String,
4520    /// The client-supplied uuids of the user messages the stopped turn was
4521    /// running (uuid-less members omitted; may be empty).
4522    #[serde(default)]
4523    pub preempted_message_uuids: Vec<String>,
4524    pub uuid: String,
4525    pub session_id: String,
4526    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4527    pub extra: serde_json::Map<String, Value>,
4528}
4529
4530/// `system/peer_message_hold` — a cross-session (peer) message this
4531/// session's receive-side policy held rather than queued, and how that hold
4532/// resolved. Lets a host show the human that a message arrived but has not
4533/// reached the model (and may never) instead of nothing at all; the sending
4534/// session is told separately over its own transport where one exists.
4535/// Informational only — there is no host-side approval through this frame.
4536/// Emitted in `-p`/SDK sessions (CLI 2.1.273+).
4537#[derive(Debug, Clone, Serialize, Deserialize)]
4538pub struct PeerMessageHoldMessage {
4539    /// `held` once per message per hold cause (a re-announcement under a
4540    /// different cause emits again); then at most one of `released` (it
4541    /// enters the queue — its `command_lifecycle` `queued` and user replay
4542    /// echo follow under `message_uuid`) or `dropped` (it will never reach
4543    /// the model in this session; see `outcome`).
4544    pub state: PeerMessageHoldState,
4545    /// The parked command's uuid — the value its later `command_lifecycle`
4546    /// frames and user replay echo carry. For lane `bridge`/`stdin` it is
4547    /// the id the host supplied on the inbound message; for lane `socket` it
4548    /// was chosen by the sending session, so correlate it only within
4549    /// `peer_message_hold` / peer replay frames, never against the host's
4550    /// own prompts' lifecycle.
4551    #[serde(default, skip_serializing_if = "Option::is_none")]
4552    pub message_uuid: Option<String>,
4553    /// Which ingress delivered it.
4554    pub lane: PeerMessageLane,
4555    /// The sender's address as the envelope will show it — an address-shaped
4556    /// token with control and invisible code points scrubbed; empty when the
4557    /// sender supplied none or an unshaped one. Sender-asserted on the
4558    /// socket lane: a label, not an identity proof.
4559    pub from: String,
4560    /// The sender's display name when it supplied one, normalized and
4561    /// scrubbed the same way. A claim, like `from`.
4562    #[serde(default, skip_serializing_if = "Option::is_none")]
4563    pub from_name: Option<String>,
4564    /// State `held` only: why the policy parked it.
4565    #[serde(default, skip_serializing_if = "Option::is_none")]
4566    pub cause: Option<PeerMessageHoldCause>,
4567    /// State `dropped` only: how the hold ended.
4568    #[serde(default, skip_serializing_if = "Option::is_none")]
4569    pub outcome: Option<PeerMessageHoldOutcome>,
4570    pub uuid: String,
4571    pub session_id: String,
4572    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4573    pub extra: serde_json::Map<String, Value>,
4574}
4575
4576/// Lifecycle state carried by a [`PeerMessageHoldMessage`].
4577#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4578pub enum PeerMessageHoldState {
4579    /// The receive-side policy parked the message instead of queueing it.
4580    Held,
4581    /// The policy now accepts it: it enters the queue.
4582    Released,
4583    /// It will never reach the model in this session.
4584    Dropped,
4585    /// A state not yet known to this version of the crate.
4586    Unknown(String),
4587}
4588
4589impl PeerMessageHoldState {
4590    pub fn as_str(&self) -> &str {
4591        match self {
4592            Self::Held => "held",
4593            Self::Released => "released",
4594            Self::Dropped => "dropped",
4595            Self::Unknown(s) => s.as_str(),
4596        }
4597    }
4598}
4599
4600impl fmt::Display for PeerMessageHoldState {
4601    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4602        f.write_str(self.as_str())
4603    }
4604}
4605
4606impl From<&str> for PeerMessageHoldState {
4607    fn from(s: &str) -> Self {
4608        match s {
4609            "held" => Self::Held,
4610            "released" => Self::Released,
4611            "dropped" => Self::Dropped,
4612            other => Self::Unknown(other.to_string()),
4613        }
4614    }
4615}
4616
4617impl Serialize for PeerMessageHoldState {
4618    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4619        serializer.serialize_str(self.as_str())
4620    }
4621}
4622
4623impl<'de> Deserialize<'de> for PeerMessageHoldState {
4624    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4625        let s = String::deserialize(deserializer)?;
4626        Ok(Self::from(s.as_str()))
4627    }
4628}
4629
4630/// Which ingress delivered the message a [`PeerMessageHoldMessage`] reports.
4631#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4632pub enum PeerMessageLane {
4633    /// The Remote Control bridge.
4634    Bridge,
4635    /// The host's own stdin stream.
4636    Stdin,
4637    /// The local cross-session socket.
4638    Socket,
4639    /// A lane not yet known to this version of the crate.
4640    Unknown(String),
4641}
4642
4643impl PeerMessageLane {
4644    pub fn as_str(&self) -> &str {
4645        match self {
4646            Self::Bridge => "bridge",
4647            Self::Stdin => "stdin",
4648            Self::Socket => "socket",
4649            Self::Unknown(s) => s.as_str(),
4650        }
4651    }
4652}
4653
4654impl fmt::Display for PeerMessageLane {
4655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4656        f.write_str(self.as_str())
4657    }
4658}
4659
4660impl From<&str> for PeerMessageLane {
4661    fn from(s: &str) -> Self {
4662        match s {
4663            "bridge" => Self::Bridge,
4664            "stdin" => Self::Stdin,
4665            "socket" => Self::Socket,
4666            other => Self::Unknown(other.to_string()),
4667        }
4668    }
4669}
4670
4671impl Serialize for PeerMessageLane {
4672    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4673        serializer.serialize_str(self.as_str())
4674    }
4675}
4676
4677impl<'de> Deserialize<'de> for PeerMessageLane {
4678    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4679        let s = String::deserialize(deserializer)?;
4680        Ok(Self::from(s.as_str()))
4681    }
4682}
4683
4684/// Why the receive-side policy parked a peer message
4685/// ([`PeerMessageHoldMessage::cause`], state `held` only). The `*Setting`
4686/// causes are a standing `crossSessionInbound: "hold"`; `ModeMismatch` and
4687/// `NoModeAsserted` are the permission-mode parity holds (the sender runs in
4688/// a different permission class, or asserted none while this session
4689/// bypasses permissions).
4690#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4691pub enum PeerMessageHoldCause {
4692    ExplicitSetting,
4693    ManagedSetting,
4694    RepoSetting,
4695    InvalidSetting,
4696    BypassDefault,
4697    ModeUnknown,
4698    ModeMismatch,
4699    NoModeAsserted,
4700    /// A cause not yet known to this version of the crate.
4701    Unknown(String),
4702}
4703
4704impl PeerMessageHoldCause {
4705    pub fn as_str(&self) -> &str {
4706        match self {
4707            Self::ExplicitSetting => "explicit-setting",
4708            Self::ManagedSetting => "managed-setting",
4709            Self::RepoSetting => "repo-setting",
4710            Self::InvalidSetting => "invalid-setting",
4711            Self::BypassDefault => "bypass-default",
4712            Self::ModeUnknown => "mode-unknown",
4713            Self::ModeMismatch => "mode-mismatch",
4714            Self::NoModeAsserted => "no-mode-asserted",
4715            Self::Unknown(s) => s.as_str(),
4716        }
4717    }
4718}
4719
4720impl fmt::Display for PeerMessageHoldCause {
4721    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4722        f.write_str(self.as_str())
4723    }
4724}
4725
4726impl From<&str> for PeerMessageHoldCause {
4727    fn from(s: &str) -> Self {
4728        match s {
4729            "explicit-setting" => Self::ExplicitSetting,
4730            "managed-setting" => Self::ManagedSetting,
4731            "repo-setting" => Self::RepoSetting,
4732            "invalid-setting" => Self::InvalidSetting,
4733            "bypass-default" => Self::BypassDefault,
4734            "mode-unknown" => Self::ModeUnknown,
4735            "mode-mismatch" => Self::ModeMismatch,
4736            "no-mode-asserted" => Self::NoModeAsserted,
4737            other => Self::Unknown(other.to_string()),
4738        }
4739    }
4740}
4741
4742impl Serialize for PeerMessageHoldCause {
4743    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4744        serializer.serialize_str(self.as_str())
4745    }
4746}
4747
4748impl<'de> Deserialize<'de> for PeerMessageHoldCause {
4749    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4750        let s = String::deserialize(deserializer)?;
4751        Ok(Self::from(s.as_str()))
4752    }
4753}
4754
4755/// How a peer-message hold ended ([`PeerMessageHoldMessage::outcome`],
4756/// state `dropped` only).
4757#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4758pub enum PeerMessageHoldOutcome {
4759    /// No one approved it before the approval deadline (a headless host has
4760    /// no approval surface, so every parity hold ends this way unless the
4761    /// mode changes first).
4762    Expired,
4763    /// The policy turned to refuse while it waited.
4764    Refused,
4765    /// Released or approved, but the ingress guard (rate limit, duplicate,
4766    /// queue cap) discarded it.
4767    Dropped,
4768    /// The session ended with it still parked.
4769    Discarded,
4770    /// An outcome not yet known to this version of the crate.
4771    Unknown(String),
4772}
4773
4774impl PeerMessageHoldOutcome {
4775    pub fn as_str(&self) -> &str {
4776        match self {
4777            Self::Expired => "expired",
4778            Self::Refused => "refused",
4779            Self::Dropped => "dropped",
4780            Self::Discarded => "discarded",
4781            Self::Unknown(s) => s.as_str(),
4782        }
4783    }
4784}
4785
4786impl fmt::Display for PeerMessageHoldOutcome {
4787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4788        f.write_str(self.as_str())
4789    }
4790}
4791
4792impl From<&str> for PeerMessageHoldOutcome {
4793    fn from(s: &str) -> Self {
4794        match s {
4795            "expired" => Self::Expired,
4796            "refused" => Self::Refused,
4797            "dropped" => Self::Dropped,
4798            "discarded" => Self::Discarded,
4799            other => Self::Unknown(other.to_string()),
4800        }
4801    }
4802}
4803
4804impl Serialize for PeerMessageHoldOutcome {
4805    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4806        serializer.serialize_str(self.as_str())
4807    }
4808}
4809
4810impl<'de> Deserialize<'de> for PeerMessageHoldOutcome {
4811    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4812        let s = String::deserialize(deserializer)?;
4813        Ok(Self::from(s.as_str()))
4814    }
4815}
4816
4817/// `system/per_turn_effort_changed` — the conversation stopped sending effort
4818/// per turn, because the server refused its per-turn effort message or a
4819/// `role:"system"` message. From the retried request on, an effort change
4820/// rewrites the cached prefix, for every model, until a later `system/init`
4821/// says otherwise. Sent once, when the conversation first turns per-turn
4822/// effort off, before the retried request's output; sent again only if the
4823/// server refuses again after the conversation's refusals are cleared (for
4824/// example by `/clear` or a compaction). It can repeat a `false` the host
4825/// already holds. Pairs with [`InitMessage::per_turn_effort_active`]
4826/// (CLI 2.1.281+).
4827#[derive(Debug, Clone, Serialize, Deserialize)]
4828pub struct PerTurnEffortChangedMessage {
4829    /// Always `false`: a change to `true` is reported only by `system/init`.
4830    pub per_turn_effort_active: bool,
4831    pub uuid: String,
4832    pub session_id: String,
4833}
4834
4835/// `system/instruction_size_warning` — the session's instruction files
4836/// (CLAUDE.md files, rules files and the files they import) add up to more
4837/// than the recommended limit. Numbers only: the host words the warning in
4838/// the person's language. Sent to Claude Desktop's Code tab alone, at most
4839/// once per process, on a conversation's first turn after `system/init`;
4840/// never stored in the transcript. Safe to ignore (CLI 2.1.288+).
4841#[derive(Debug, Clone, Serialize, Deserialize)]
4842pub struct InstructionSizeWarningMessage {
4843    /// Combined length, in characters, of the instruction files in the
4844    /// session prompt. Always over [`Self::total_limit_chars`].
4845    pub total_chars: u64,
4846    /// The recommended limit for `total_chars` on the turn's model, in
4847    /// characters.
4848    pub total_limit_chars: u64,
4849    /// Number of files counted in `total_chars`. When it is 1, word the
4850    /// warning for a single file rather than a total.
4851    pub file_count: u64,
4852    /// Length of the largest file, in characters. Present only when the
4853    /// warning should mention it: there are several files and this one by
4854    /// itself is over `total_limit_chars`.
4855    #[serde(default, skip_serializing_if = "Option::is_none")]
4856    pub largest_chars: Option<u64>,
4857    pub uuid: String,
4858    pub session_id: String,
4859}
4860
4861/// `system/file_attachments_missing` — some files a user message sent by
4862/// reference (`file_attachments`) did not arrive where Claude can read them,
4863/// and Claude was told so; the turn runs on without them. A host can use it
4864/// to tell the person who sent them. Only sent by a worker connected to
4865/// Claude Code Remote. Each event states the whole list for its message, so
4866/// keep the last one per `message_uuid` (CLI 2.1.290+).
4867#[derive(Debug, Clone, Serialize, Deserialize)]
4868pub struct FileAttachmentsMissingMessage {
4869    /// The uuid of the inbound user message whose `file_attachments` named
4870    /// the files.
4871    pub message_uuid: String,
4872    /// How many well-formed `file_attachments` entries that message had.
4873    pub sent_count: u64,
4874    /// The files Claude was told did not arrive, each once. Never empty.
4875    pub missing: Vec<MissingFileAttachment>,
4876    pub uuid: String,
4877    pub session_id: String,
4878}
4879
4880/// `system/permission_check_status` — a tool call has been waiting on its
4881/// automatic permission check (the auto-mode classifier) for longer than
4882/// usual, so a host can say the step is being checked instead of showing a
4883/// silent pause. Every `checking` is followed by a `done` for the same
4884/// `tool_use_id`; `done` does not say whether the call was allowed (the
4885/// tool's result, `permission_denied` or a `can_use_tool` request does). One
4886/// call can send more than one pair. Display-only and best-effort: also stop
4887/// showing it at any other frame for this `tool_use_id` or at the end of the
4888/// turn (CLI 2.1.292+).
4889#[derive(Debug, Clone, Serialize, Deserialize)]
4890pub struct PermissionCheckStatusMessage {
4891    pub tool_use_id: String,
4892    /// Subagent ID when the tool call originated inside a subagent, as on
4893    /// `permission_denied`.
4894    #[serde(default, skip_serializing_if = "Option::is_none")]
4895    pub agent_id: Option<String>,
4896    pub status: PermissionCheckStatus,
4897    pub uuid: String,
4898    pub session_id: String,
4899}
4900
4901/// `system/inherited_asks_settled` — a cloud worker's report, before its
4902/// first turn, of which asks it inherited from the earlier worker and keeps
4903/// answerable. Any ask stored before this event that it does not name is
4904/// over (CLI 2.1.311+).
4905#[derive(Debug, Clone, Serialize, Deserialize)]
4906pub struct InheritedAsksSettledMessage {
4907    /// The request ids of the earlier worker's asks that this worker keeps
4908    /// answerable.
4909    pub kept_request_ids: Vec<String>,
4910    pub uuid: String,
4911    pub session_id: String,
4912}
4913
4914/// Phase of a [`PermissionCheckStatusMessage`].
4915#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4916pub enum PermissionCheckStatus {
4917    /// The call has waited about four seconds on its check and is still
4918    /// waiting.
4919    Checking,
4920    /// That wait is over.
4921    Done,
4922    /// A status not yet known to this version of the crate.
4923    Unknown(String),
4924}
4925
4926impl PermissionCheckStatus {
4927    pub fn as_str(&self) -> &str {
4928        match self {
4929            Self::Checking => "checking",
4930            Self::Done => "done",
4931            Self::Unknown(s) => s.as_str(),
4932        }
4933    }
4934}
4935
4936impl fmt::Display for PermissionCheckStatus {
4937    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4938        f.write_str(self.as_str())
4939    }
4940}
4941
4942impl From<&str> for PermissionCheckStatus {
4943    fn from(s: &str) -> Self {
4944        match s {
4945            "checking" => Self::Checking,
4946            "done" => Self::Done,
4947            other => Self::Unknown(other.to_string()),
4948        }
4949    }
4950}
4951
4952impl Serialize for PermissionCheckStatus {
4953    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4954        serializer.serialize_str(self.as_str())
4955    }
4956}
4957
4958impl<'de> Deserialize<'de> for PermissionCheckStatus {
4959    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4960        let s = String::deserialize(deserializer)?;
4961        Ok(Self::from(s.as_str()))
4962    }
4963}
4964
4965/// One entry of [`FileAttachmentsMissingMessage::missing`].
4966#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4967pub struct MissingFileAttachment {
4968    /// The `file_uuid` of the message's `file_attachments` entry.
4969    pub file_uuid: String,
4970    /// Why, as a short code: `download`, `too_large`, `not_found` (the
4971    /// files route answered 404 or 410, so sending it again rarely helps;
4972    /// CLI 2.1.293+), `write`, `no_token`, `not_finished` or `count_cap`.
4973    /// More codes may be added; read an unknown one as "did not arrive".
4974    pub reason: String,
4975}
4976
4977/// `system/session_title_changed` — the session's name, for a host that
4978/// shows it (an IDE editor tab, say). A headless session sends it at startup
4979/// when the session already has a name, and again after each change to the
4980/// name (not an AI-generated title), including a rename the host itself sent
4981/// with `rename_session`. A cleared name is not sent. The same name can
4982/// arrive more than once, so treat a repeated name as no change. The frame
4983/// can arrive before the `system/init` that first names its `session_id`
4984/// (at startup or after `/clear`); a host that learns the session id from
4985/// init can hold the name until then (CLI 2.1.285+).
4986#[derive(Debug, Clone, Serialize, Deserialize)]
4987pub struct SessionTitleChangedMessage {
4988    /// The session's current name: control, bidirectional and zero-width
4989    /// characters replaced with single spaces, ends trimmed, at most 200
4990    /// Unicode code points. A suffix can make it unique when another session
4991    /// already has the name.
4992    pub title: String,
4993    pub uuid: String,
4994    pub session_id: String,
4995}
4996
4997/// `system/session_writes_lost` — the CLI could not save lines of a session
4998/// transcript, changes to `~/.claude.json`, or both, because the disk was
4999/// full or its quota was used up. Sent at most once, as the CLI exits with
5000/// status 0, after the last result if there was one. A run that exits 1, as
5001/// after an error result, gets only the line on stderr (CLI 2.1.317 and some
5002/// later releases sent this message there too). The results already received stand,
5003/// and this message does not change the exit status. Sent only with
5004/// stream-json output on stdout, never under `--sdk-url` or a Claude Desktop
5005/// entrypoint. Safe to ignore (CLI 2.1.317+).
5006#[derive(Debug, Clone, Serialize, Deserialize)]
5007pub struct SessionWritesLostMessage {
5008    /// Why the writes failed. When both kinds of write were lost for
5009    /// different reasons, the transcript's.
5010    pub error_code: SessionWriteErrorCode,
5011    /// What was not saved. Never empty.
5012    pub lost: Vec<LostSessionWrite>,
5013    /// The sessions whose transcripts lost lines, in the order each first
5014    /// lost one. Present only when [`LostSessionWrite::Transcript`] is
5015    /// listed; never empty. Usually `session_id` alone; a CLI that started a
5016    /// new session after the loss (as `/clear` does) lists the earlier
5017    /// session, and lists `session_id` only if that session lost lines too.
5018    #[serde(default, skip_serializing_if = "Option::is_none")]
5019    pub transcript_session_ids: Option<Vec<String>>,
5020    pub uuid: String,
5021    /// The session the CLI is in, as on every message; see
5022    /// [`transcript_session_ids`](Self::transcript_session_ids) for whose
5023    /// transcript lost lines.
5024    pub session_id: String,
5025}
5026
5027/// [`SessionWritesLostMessage::error_code`]: why the writes failed.
5028#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5029pub enum SessionWriteErrorCode {
5030    /// `ENOSPC` — the disk was full.
5031    Enospc,
5032    /// `EDQUOT` — the disk quota was used up.
5033    Edquot,
5034    /// A code not yet known to this version of the crate.
5035    Unknown(String),
5036}
5037
5038impl SessionWriteErrorCode {
5039    pub fn as_str(&self) -> &str {
5040        match self {
5041            Self::Enospc => "ENOSPC",
5042            Self::Edquot => "EDQUOT",
5043            Self::Unknown(s) => s.as_str(),
5044        }
5045    }
5046}
5047
5048impl fmt::Display for SessionWriteErrorCode {
5049    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5050        f.write_str(self.as_str())
5051    }
5052}
5053
5054impl From<&str> for SessionWriteErrorCode {
5055    fn from(s: &str) -> Self {
5056        match s {
5057            "ENOSPC" => Self::Enospc,
5058            "EDQUOT" => Self::Edquot,
5059            other => Self::Unknown(other.to_string()),
5060        }
5061    }
5062}
5063
5064impl Serialize for SessionWriteErrorCode {
5065    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5066        serializer.serialize_str(self.as_str())
5067    }
5068}
5069
5070impl<'de> Deserialize<'de> for SessionWriteErrorCode {
5071    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5072        let s = String::deserialize(deserializer)?;
5073        Ok(Self::from(s.as_str()))
5074    }
5075}
5076
5077/// One entry of [`SessionWritesLostMessage::lost`]: what was not saved.
5078#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5079pub enum LostSessionWrite {
5080    /// Lines of a session transcript, so resuming a session in
5081    /// `transcript_session_ids` may miss part of its conversation.
5082    Transcript,
5083    /// Changes this run made to `~/.claude.json`.
5084    GlobalConfig,
5085    /// A value not yet known to this version of the crate.
5086    Unknown(String),
5087}
5088
5089impl LostSessionWrite {
5090    pub fn as_str(&self) -> &str {
5091        match self {
5092            Self::Transcript => "transcript",
5093            Self::GlobalConfig => "global_config",
5094            Self::Unknown(s) => s.as_str(),
5095        }
5096    }
5097}
5098
5099impl fmt::Display for LostSessionWrite {
5100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5101        f.write_str(self.as_str())
5102    }
5103}
5104
5105impl From<&str> for LostSessionWrite {
5106    fn from(s: &str) -> Self {
5107        match s {
5108            "transcript" => Self::Transcript,
5109            "global_config" => Self::GlobalConfig,
5110            other => Self::Unknown(other.to_string()),
5111        }
5112    }
5113}
5114
5115impl Serialize for LostSessionWrite {
5116    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5117        serializer.serialize_str(self.as_str())
5118    }
5119}
5120
5121impl<'de> Deserialize<'de> for LostSessionWrite {
5122    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5123        let s = String::deserialize(deserializer)?;
5124        Ok(Self::from(s.as_str()))
5125    }
5126}
5127
5128/// `system/ui_log` — one `$.ui.log(text)` line from a plugin in a headless
5129/// session: what the REPL appends to its transcript as a dim notice under
5130/// the plugin's name. Not sent to the model (CLI 2.1.285+).
5131#[derive(Debug, Clone, Serialize, Deserialize)]
5132pub struct UiLogMessage {
5133    /// The calling plugin's registry name, as the hooks engine knows it.
5134    pub plugin: String,
5135    /// The line the plugin gave: be ready for up to 10000 characters of it
5136    /// and then an ellipsis (4096 before CLI 2.1.289). Plain text: draw it
5137    /// as a text node, never as markup.
5138    pub text: String,
5139    pub uuid: String,
5140    pub session_id: String,
5141}
5142
5143/// `system/ui_notify` — one `$.ui.notify(text)` from a plugin in a headless
5144/// session: a notification for the client to raise under the plugin's name,
5145/// now or later. Nothing limits how often a plugin sends one
5146/// (CLI 2.1.321+).
5147#[derive(Debug, Clone, Serialize, Deserialize)]
5148pub struct UiNotifyMessage {
5149    /// The calling plugin's registry name, as the hooks engine knows it
5150    /// (never plugin-settable). Not cleaned as `title` is, so scrub it, and
5151    /// show it after the host's own word that a mod sent it, so neither it
5152    /// nor the title can pass for the app's own.
5153    pub plugin: String,
5154    /// The plugin's full id, `name@marketplace` (`name@inline` when
5155    /// side-loaded), as the hooks engine loaded it (never plugin-settable).
5156    /// Two plugins sharing a name differ here.
5157    pub source: String,
5158    /// The notification's heading: the plugin's words, cleaned of control
5159    /// characters and cut to 120 characters, or the plugin's name. Plain
5160    /// text, and never the only sign of who sent it.
5161    pub title: String,
5162    /// The body: the plugin's words, cleaned of control characters and cut
5163    /// to 1000 characters. Plain text: draw it as a text node, never as
5164    /// markup.
5165    pub text: String,
5166    /// The plugin's name for this notification: one with the same `source`
5167    /// and key replaces the last. Scope it by `source`, so one mod cannot
5168    /// touch another's. Absent: each frame is its own notification.
5169    #[serde(default, skip_serializing_if = "Option::is_none")]
5170    pub key: Option<String>,
5171    /// How long after this frame arrives to raise it; the client holds it,
5172    /// so it fires with the session out of reach. At most 7 days. Absent
5173    /// (with no [`at`](Self::at)): raise it now.
5174    #[serde(default, skip_serializing_if = "Option::is_none")]
5175    pub delay_ms: Option<u64>,
5176    /// The epoch-millisecond time to raise it, at most 7 days ahead. Never
5177    /// sent beside [`delay_ms`](Self::delay_ms).
5178    #[serde(default, skip_serializing_if = "Option::is_none")]
5179    pub at: Option<u64>,
5180    /// With `delay_ms` or `at`: throw it away if the person returns to the
5181    /// session before it fires.
5182    #[serde(default, skip_serializing_if = "Option::is_none")]
5183    pub until_back: Option<bool>,
5184    /// True when what this frame carries differs from what the sender sent
5185    /// and another plugin's hook was in the chain: its words, timing or key
5186    /// may not be the sender's, so say so beside the sender. Always sent;
5187    /// missing (from an older engine) reads as false. Never plugin-settable.
5188    #[serde(default)]
5189    pub rewritten: bool,
5190    pub uuid: String,
5191    pub session_id: String,
5192}
5193
5194/// `system/ui_notify_cancel` — one `$.ui.cancelNotify(key)` from a plugin in
5195/// a headless session, or the engine removing a plugin: remove the
5196/// notification with this `source` and `key`, shown or still waiting. None
5197/// held under that key: do nothing (CLI 2.1.321+).
5198#[derive(Debug, Clone, Serialize, Deserialize)]
5199pub struct UiNotifyCancelMessage {
5200    /// The plugin's registry name, as the hooks engine knows it (never
5201    /// plugin-settable).
5202    pub plugin: String,
5203    /// The plugin's full id, as on [`UiNotifyMessage::source`] (never
5204    /// plugin-settable). Only notifications of this source are touched.
5205    pub source: String,
5206    /// The [`UiNotifyMessage::key`] of the notification to take back.
5207    /// Absent: the engine removed the plugin from this session, so drop
5208    /// everything this source has waiting for this session. Only the engine
5209    /// sends that.
5210    #[serde(default, skip_serializing_if = "Option::is_none")]
5211    pub key: Option<String>,
5212    pub uuid: String,
5213    pub session_id: String,
5214}
5215
5216/// `system/ui_toast` — one `$.ui.toast(text)` from a plugin in a headless
5217/// session: what the REPL shows on the notification bar under the prompt
5218/// for a few seconds, under the plugin's name. Toasts arrive at no fixed
5219/// rate: CLI 2.1.289 dropped the hooks host's 2 s spacing (CLI 2.1.285+).
5220#[derive(Debug, Clone, Serialize, Deserialize)]
5221pub struct UiToastMessage {
5222    /// The calling plugin's registry name, as the hooks engine knows it.
5223    pub plugin: String,
5224    /// The line the plugin gave: be ready for up to 10000 characters of it
5225    /// and then an ellipsis (4096 before CLI 2.1.289). Plain text: draw it
5226    /// as a text node, never as markup.
5227    pub text: String,
5228    /// How long the REPL would keep it on the notification bar: the plugin's
5229    /// `timeoutMs`, or the engine's default (4000).
5230    pub timeout_ms: u64,
5231    pub uuid: String,
5232    pub session_id: String,
5233}
5234
5235/// `system/ui_status` — one `$.ui.status(text)` from a plugin in a headless
5236/// session: the line the REPL pins under the prompt until the plugin's next
5237/// call replaces or clears it. One status line per plugin (CLI 2.1.285+).
5238#[derive(Debug, Clone, Serialize, Deserialize)]
5239pub struct UiStatusMessage {
5240    /// The calling plugin's registry name, as the hooks engine knows it.
5241    pub plugin: String,
5242    /// The plugin's pinned line, replacing its previous one; `None` (wire
5243    /// `null`) removes it. Be ready for up to 10000 characters of it and
5244    /// then an ellipsis (4096 before CLI 2.1.289). Plain text: draw it as a
5245    /// text node, never as markup.
5246    pub text: Option<String>,
5247    pub uuid: String,
5248    pub session_id: String,
5249}
5250
5251/// `system/ui_invalidate` — a plugin called `$.ui.invalidate("ui.render")`,
5252/// a plugin loaded or reloaded, or (CLI 2.1.318+) a mobile client already
5253/// known to the session named a field in `draws` it had not named before:
5254/// every tree a remote surface drew through `ui_render` may be stale, and the surface re-asks each mounted instance.
5255/// Bumps from one plugin fold to at most one per 100 ms (CLI 2.1.285+).
5256#[derive(Debug, Clone, Serialize, Deserialize)]
5257pub struct UiInvalidateMessage {
5258    /// The render event whose version moved — `"ui.render"`; the engine
5259    /// versions it as one event, so no component or instance is named.
5260    pub event: String,
5261    /// Present when a plugin state write, or (CLI 2.1.289+) a `Client`
5262    /// drawn in them that failed and whose plugin heard it (`ui.fault`),
5263    /// made only these instances stale, so the surface may re-ask them
5264    /// alone. The theme of the session being set or cleared
5265    /// (`$.theme.set` / `$.theme.clear` on CLI 2.1.319+), or a client of a
5266    /// surface that draws it joining where one ever stood, names the
5267    /// `Backdrop` instance (`instance_id` `backdrop`) and, on CLI 2.1.321+,
5268    /// the `Theme` instance (`instance_id` `theme`) on each such surface.
5269    /// Absent, every mounted instance may be stale.
5270    #[serde(default, skip_serializing_if = "Option::is_none")]
5271    pub instances: Option<Vec<UiInvalidatedInstance>>,
5272    /// Present, and true, while a mod may change what is recorded of a typed
5273    /// line (a hook on `prompt.submit`, `command.run`, `session.append` or
5274    /// `session.receive`, mods not yet known, or an organization policy that
5275    /// lists a plugin or cannot be read): what the `prompts_screened_v1`
5276    /// capability says on `system/init`, sent as soon as a plugin load or
5277    /// reload makes it so. Absent says nothing; the next `system/init` does
5278    /// (CLI 2.1.300+).
5279    #[serde(default, skip_serializing_if = "Option::is_none")]
5280    pub prompts_screened: Option<bool>,
5281    pub uuid: String,
5282    pub session_id: String,
5283}
5284
5285/// One stale instance named by [`UiInvalidateMessage::instances`].
5286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5287pub struct UiInvalidatedInstance {
5288    /// The surface that drew the instance.
5289    pub surface: String,
5290    /// The component of the instance.
5291    pub component: String,
5292    /// The engine's requestId of the instance, as the `ui_render` that drew
5293    /// it named it.
5294    pub instance_id: String,
5295}
5296
5297/// `system/ui_panes` — the pane roster a remote surface draws changed: a
5298/// plugin opened, retitled or closed a pane, the person showed, focused or
5299/// closed one, or a placing client attached. Deduplicated: an unchanged
5300/// roster is not pushed. A pane opened before the stream armed produces
5301/// none, so read the roster with a `ui_panes` request on mount
5302/// (CLI 2.1.285+).
5303#[derive(Debug, Clone, Serialize, Deserialize)]
5304pub struct UiPanesMessage {
5305    /// The placed open panes in open order (an open appends, a close
5306    /// removes); a pane waiting unplaced is not listed.
5307    pub panes: Vec<UiPane>,
5308    /// The pane the surface shows: the last opened or focused, never
5309    /// displacing one the person holds; `None` with none open.
5310    pub shown_id: Option<String>,
5311    /// The pane the person gave the keyboard; `None` while the composer has
5312    /// it.
5313    pub focused_id: Option<String>,
5314    /// A standing `$.ui.open({ focus: true })` the surface has yet to judge:
5315    /// honour it only while the composer is empty and idle and no dialog is
5316    /// up, else refuse it.
5317    pub focus_requested_id: Option<String>,
5318    pub uuid: String,
5319    pub session_id: String,
5320}
5321
5322/// One placed pane in a [`UiPanesMessage`] roster.
5323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5324pub struct UiPane {
5325    /// The pane's id as the plugin opened it: 1 to 64 of letters, digits,
5326    /// `_`, `-`.
5327    pub id: String,
5328    /// The pane's tab label: the `title` its latest open gave, else its id.
5329    /// Plugin-authored text: draw it as a text node, never as markup.
5330    pub title: String,
5331    /// The registry name of the plugin that opened it.
5332    pub plugin: String,
5333    /// `true`: Escape while the pane holds the keyboard, or at an idle empty
5334    /// composer, closes it. Absent, Escape only returns the keyboard.
5335    #[serde(default, skip_serializing_if = "Option::is_none")]
5336    pub close_on_escape: Option<bool>,
5337    /// `true`: the surface holds its transient toasts while this pane is
5338    /// the one shown, and shows them once it closes.
5339    #[serde(default, skip_serializing_if = "Option::is_none")]
5340    pub hold_toasts: Option<bool>,
5341    /// What the surface paints under this pane's body while it is the one
5342    /// shown: a theme key, a raw color, or `transparent` for nothing.
5343    /// Plugin-authored: parse it, never write it into a style. Absent, the
5344    /// surface's own look stands (CLI 2.1.311+).
5345    #[serde(default, skip_serializing_if = "Option::is_none")]
5346    pub background_color: Option<String>,
5347    /// The body rows its content asked for while seated inline; a request
5348    /// the person's own sizing overrides.
5349    #[serde(default, skip_serializing_if = "Option::is_none")]
5350    pub rows: Option<u64>,
5351    /// The body columns its content asked for while docked; a request the
5352    /// person's own sizing overrides.
5353    #[serde(default, skip_serializing_if = "Option::is_none")]
5354    pub columns: Option<u64>,
5355}
5356
5357/// `system/ui_placed` — a fire-and-forget push from a headless session when
5358/// a plugin places a row with `$.ui.place`, and to a client as it joins
5359/// (CLI 2.1.314+). A surface inserts each row after
5360/// [`UiPlacedRow::after_uuid`] and asks `ui_render` for it. Display only:
5361/// no message carries these rows. A surface drops the rows it holds with
5362/// the conversation it resets, and a row whose `after_uuid` it no longer
5363/// holds. A compaction ends the rows before it, and no push says so.
5364#[derive(Debug, Clone, Serialize, Deserialize)]
5365pub struct UiPlacedMessage {
5366    /// The main conversation's placed rows, in order.
5367    pub rows: Vec<UiPlacedRow>,
5368    pub uuid: String,
5369    pub session_id: String,
5370}
5371
5372/// One row in a [`UiPlacedMessage`].
5373#[derive(Debug, Clone, Serialize, Deserialize)]
5374pub struct UiPlacedRow {
5375    /// The plugin that placed the row.
5376    pub plugin: String,
5377    /// The row's id among that plugin's rows.
5378    pub id: String,
5379    /// The `instance_id` of the `ui_render` ask, with `component: "Placed"`,
5380    /// that draws it.
5381    pub request_id: String,
5382    /// The conversation message the row follows; absent at the
5383    /// conversation's start. Rows sharing it stand in roster order.
5384    #[serde(default, skip_serializing_if = "Option::is_none")]
5385    pub after_uuid: Option<String>,
5386}
5387
5388/// `system/ui_scroll` — a plugin's `$.ui.scroll` moved the window of a site
5389/// a remote surface draws: the surface scrolls the site's body there. The
5390/// person's own scrolls are never pushed back (CLI 2.1.285+).
5391#[derive(Debug, Clone, Serialize, Deserialize)]
5392pub struct UiScrollMessage {
5393    /// The client drawing the site, as it attached.
5394    pub client_id: String,
5395    /// Which site: a pane's body or the band above the prompt.
5396    pub component: UiSiteComponent,
5397    /// The pane's id, or `above-prompt` for the band.
5398    pub instance_id: String,
5399    /// The first content row the window is to show, 0 at the top, clamped
5400    /// to the tree as the surface last reported it.
5401    pub offset: u64,
5402    /// `true`: keep the window on the tree's last rows through the draws to
5403    /// come, until the person or a plugin next moves it.
5404    #[serde(default, skip_serializing_if = "Option::is_none")]
5405    pub follow_end: Option<bool>,
5406    pub uuid: String,
5407    pub session_id: String,
5408}
5409
5410/// `system/ui_focus` — a plugin's `$.ui.focus` moved the focus ring of a
5411/// site a remote surface draws onto one of its Buttons, Inputs or Selects:
5412/// the surface focuses that element if it still draws it. The person's own
5413/// moves are never pushed back (CLI 2.1.285+).
5414#[derive(Debug, Clone, Serialize, Deserialize)]
5415pub struct UiFocusMessage {
5416    /// The client drawing the site, as it attached.
5417    pub client_id: String,
5418    /// Which site: a pane's body or the band above the prompt.
5419    pub component: UiSiteComponent,
5420    /// The pane's id, or `above-prompt` for the band.
5421    pub instance_id: String,
5422    /// Whose element takes the ring: its `press.plugin`.
5423    pub plugin: String,
5424    /// The element's `props.key`, as `ui.press` names it.
5425    pub key: String,
5426    pub uuid: String,
5427    pub session_id: String,
5428}
5429
5430/// The site a [`UiScrollMessage`] or [`UiFocusMessage`] addresses.
5431#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5432pub enum UiSiteComponent {
5433    /// A pane's body.
5434    Pane,
5435    /// The band above the prompt.
5436    AbovePrompt,
5437    /// A site not yet known to this version of the crate.
5438    Unknown(String),
5439}
5440
5441impl UiSiteComponent {
5442    pub fn as_str(&self) -> &str {
5443        match self {
5444            Self::Pane => "Pane",
5445            Self::AbovePrompt => "AbovePrompt",
5446            Self::Unknown(s) => s.as_str(),
5447        }
5448    }
5449}
5450
5451impl fmt::Display for UiSiteComponent {
5452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5453        f.write_str(self.as_str())
5454    }
5455}
5456
5457impl From<&str> for UiSiteComponent {
5458    fn from(s: &str) -> Self {
5459        match s {
5460            "Pane" => Self::Pane,
5461            "AbovePrompt" => Self::AbovePrompt,
5462            other => Self::Unknown(other.to_string()),
5463        }
5464    }
5465}
5466
5467impl Serialize for UiSiteComponent {
5468    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5469        serializer.serialize_str(self.as_str())
5470    }
5471}
5472
5473impl<'de> Deserialize<'de> for UiSiteComponent {
5474    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5475        let s = String::deserialize(deserializer)?;
5476        Ok(Self::from(s.as_str()))
5477    }
5478}
5479
5480/// The run that printed a local-command row, carried as
5481/// [`AssistantMessage::local_command_run`].
5482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5483pub struct LocalCommandRun {
5484    /// The command's name as `command.run` carried it (no slash).
5485    pub command: String,
5486    /// Its arguments as the echo shows them (`***` when the command marks
5487    /// them sensitive).
5488    pub args: String,
5489    /// For a command a plugin registered, that plugin's name; for a row
5490    /// whose text plugins' `command.run` hooks answered or retold, the names
5491    /// the text is led by. Untrusted text; absent for the binary's own
5492    /// command in its own words (CLI 2.1.300+).
5493    #[serde(default, skip_serializing_if = "Option::is_none")]
5494    pub plugin: Option<String>,
5495}
5496
5497/// What a local-command row reports, carried as
5498/// [`AssistantMessage::local_command_outcome`] so a host can offer a fix
5499/// instead of relaying the text (CLI 2.1.281+).
5500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5501pub struct LocalCommandOutcome {
5502    pub kind: LocalCommandOutcomeKind,
5503    /// Kind `unknown` only: the registered command closest to the typed
5504    /// name, no slash.
5505    #[serde(default, skip_serializing_if = "Option::is_none")]
5506    pub suggestion: Option<String>,
5507}
5508
5509/// The outcome a local-command row reports ([`LocalCommandOutcome::kind`]).
5510/// New kinds are additive; the CLI itself drops the whole outcome when it
5511/// meets a kind it does not know, so treat [`Unknown`](Self::Unknown) as
5512/// absent.
5513#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5514pub enum LocalCommandOutcomeKind {
5515    /// An interactive-panel command this (headless) session cannot open.
5516    UnavailableHeadless,
5517    /// No command has the typed name; see
5518    /// [`LocalCommandOutcome::suggestion`].
5519    Unknown,
5520    /// The command ran and failed.
5521    Failed,
5522    /// The change applies after Claude Code restarts.
5523    RestartRequired,
5524    /// A kind not yet known to this version of the crate.
5525    Other(String),
5526}
5527
5528impl LocalCommandOutcomeKind {
5529    pub fn as_str(&self) -> &str {
5530        match self {
5531            Self::UnavailableHeadless => "unavailable_headless",
5532            Self::Unknown => "unknown",
5533            Self::Failed => "failed",
5534            Self::RestartRequired => "restart_required",
5535            Self::Other(s) => s.as_str(),
5536        }
5537    }
5538}
5539
5540impl fmt::Display for LocalCommandOutcomeKind {
5541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5542        f.write_str(self.as_str())
5543    }
5544}
5545
5546impl From<&str> for LocalCommandOutcomeKind {
5547    fn from(s: &str) -> Self {
5548        match s {
5549            "unavailable_headless" => Self::UnavailableHeadless,
5550            "unknown" => Self::Unknown,
5551            "failed" => Self::Failed,
5552            "restart_required" => Self::RestartRequired,
5553            other => Self::Other(other.to_string()),
5554        }
5555    }
5556}
5557
5558impl Serialize for LocalCommandOutcomeKind {
5559    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5560        serializer.serialize_str(self.as_str())
5561    }
5562}
5563
5564impl<'de> Deserialize<'de> for LocalCommandOutcomeKind {
5565    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5566        let s = String::deserialize(deserializer)?;
5567        Ok(Self::from(s.as_str()))
5568    }
5569}
5570
5571/// Structured twin of a `/usage` result, carried as
5572/// [`AssistantMessage::usage_report`]: the session's totals, the plan's
5573/// usage rows as the server sent them and the extra-usage spend, and nothing
5574/// else from the usage body (the `get_usage` control reply carries the
5575/// rest). Experimental — the shape may change.
5576#[derive(Debug, Clone, Serialize, Deserialize)]
5577pub struct UsageReport {
5578    /// Cost and usage accumulated by the current session.
5579    pub session: super::control::UsageSession,
5580    /// The plan's usage rows and extra-usage spend from the claude.ai usage
5581    /// endpoint; `None` when the CLI could not fetch them (no plan on this
5582    /// lane, or a token without the profile scope).
5583    pub rate_limits: Option<UsageReportRateLimits>,
5584    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
5585    pub extra: serde_json::Map<String, Value>,
5586}
5587
5588/// The plan's usage rows and extra-usage spend in a [`UsageReport`].
5589#[derive(Debug, Clone, Serialize, Deserialize)]
5590pub struct UsageReportRateLimits {
5591    /// The server's usage rows (the usage endpoint's `limits[]`), as sent:
5592    /// which meters apply, their scope, labels, severity and order are the
5593    /// server's, so a client renders them verbatim. Empty when the server
5594    /// reported no meters; `None` when the body carried no rows at all (a
5595    /// server that predates them). When the usage fetch failed and the CLI
5596    /// fell back to rate-limit response headers, this holds at most the one
5597    /// row it synthesizes from them.
5598    pub limits: Option<Vec<UsageReportLimit>>,
5599    /// Extra-usage (overage) spend for the billing period, when the plan
5600    /// has it.
5601    #[serde(default, skip_serializing_if = "Option::is_none")]
5602    pub extra_usage: Option<UsageReportExtraUsage>,
5603    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
5604    pub extra: serde_json::Map<String, Value>,
5605}
5606
5607/// One server usage row in [`UsageReportRateLimits::limits`].
5608#[derive(Debug, Clone, Serialize, Deserialize)]
5609pub struct UsageReportLimit {
5610    /// The server's meter kind, e.g. `session`, `weekly_all` or
5611    /// `weekly_scoped`. Classify a row on this, never on a label.
5612    pub kind: String,
5613    /// The server's row group, e.g. `session` or `weekly`; rows render
5614    /// grouped under it, in the server's order.
5615    pub group: String,
5616    /// Share of the window used, 0–100.
5617    pub percent: f64,
5618    /// ISO 8601 timestamp when the window resets.
5619    pub resets_at: Option<String>,
5620    /// What a scoped row is for, a model or a surface, with the server's
5621    /// display label.
5622    #[serde(default, skip_serializing_if = "Option::is_none")]
5623    pub scope: Option<UsageReportScope>,
5624    /// The server's reading of the row for a meter's colour, e.g. `normal`,
5625    /// `warning` or `critical`; a client falls back to its own thresholds
5626    /// without it.
5627    #[serde(default, skip_serializing_if = "Option::is_none")]
5628    pub severity: Option<String>,
5629    /// The server's headline pick: the row a single-value indicator shows.
5630    #[serde(default, skip_serializing_if = "Option::is_none")]
5631    pub is_active: Option<bool>,
5632    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
5633    pub extra: serde_json::Map<String, Value>,
5634}
5635
5636/// What a scoped [`UsageReportLimit`] row is for.
5637#[derive(Debug, Clone, Serialize, Deserialize)]
5638pub struct UsageReportScope {
5639    #[serde(default, skip_serializing_if = "Option::is_none")]
5640    pub model: Option<UsageReportScopeLabel>,
5641    #[serde(default, skip_serializing_if = "Option::is_none")]
5642    pub surface: Option<UsageReportScopeLabel>,
5643    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
5644    pub extra: serde_json::Map<String, Value>,
5645}
5646
5647/// The server's display label for a [`UsageReportScope`] member.
5648#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5649pub struct UsageReportScopeLabel {
5650    pub display_name: String,
5651}
5652
5653/// Extra-usage (overage) spend for the billing period in a
5654/// [`UsageReportRateLimits`]. Amounts are in minor units of `currency`
5655/// (cents for USD).
5656#[derive(Debug, Clone, Serialize, Deserialize)]
5657pub struct UsageReportExtraUsage {
5658    /// `false` while extra usage cannot cover sends.
5659    pub is_enabled: bool,
5660    pub monthly_limit: Option<f64>,
5661    pub used_credits: Option<f64>,
5662    pub utilization: Option<f64>,
5663    #[serde(default, skip_serializing_if = "Option::is_none")]
5664    pub currency: Option<String>,
5665    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
5666    pub extra: serde_json::Map<String, Value>,
5667}
5668
5669/// `{id, name}` of an original `Batch*` tool_use block, carried in
5670/// [`AssistantMessage::batch_tool_uses`].
5671#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5672pub struct BatchToolUse {
5673    pub id: String,
5674    pub name: String,
5675}
5676
5677/// Structured twin of the `/context` report, carried as
5678/// [`AssistantMessage::context_usage`] — the data a client needs to render
5679/// the context-usage card without parsing the markdown table. Evolves
5680/// additively; a breaking reshape would ship as a sibling field.
5681#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5682pub struct ContextUsage {
5683    /// Main-loop model the usage was computed for.
5684    pub model: String,
5685    /// Estimated tokens in use, unclamped — may exceed `raw_max_tokens` when
5686    /// over limit.
5687    pub total_tokens: u64,
5688    /// The window usage is measured against: the resolved autocompact window —
5689    /// the model's believed limit, or a smaller compaction-policy window.
5690    pub raw_max_tokens: u64,
5691    /// Rounded `total_tokens / raw_max_tokens`, 0–100+.
5692    pub percentage: u64,
5693    /// Present when `total_tokens` exceeds `raw_max_tokens`.
5694    #[serde(default, skip_serializing_if = "Option::is_none")]
5695    pub over_limit: Option<ContextOverLimit>,
5696    /// Usage-by-category rows (`Messages`, `System prompt`, …).
5697    #[serde(default)]
5698    pub categories: Vec<ContextCategory>,
5699    /// Per-tool token contributions of MCP tools.
5700    #[serde(default)]
5701    pub mcp_tools: Vec<ContextMcpTool>,
5702    /// Per-file token contributions of memory files.
5703    #[serde(default)]
5704    pub memory_files: Vec<ContextMemoryFile>,
5705    /// Per-agent token contributions of agent definitions.
5706    #[serde(default)]
5707    pub agents: Vec<ContextAgent>,
5708    /// Per-skill token contributions. Omitted when no skills contribute.
5709    #[serde(default, skip_serializing_if = "Option::is_none")]
5710    pub skills: Option<Vec<ContextSkill>>,
5711}
5712
5713/// Why and by how much a [`ContextUsage`] exceeds its window.
5714#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5715pub struct ContextOverLimit {
5716    pub tokens_over: u64,
5717    /// How the window was resolved: `"hard_limit"` (the model's believed
5718    /// limit) or `"compaction_window"` (a compaction-policy window).
5719    pub kind: String,
5720}
5721
5722/// One row of the `/context` usage-by-category breakdown.
5723#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5724pub struct ContextCategory {
5725    /// Display name of the row as the CLI renders it, e.g. `"Messages"`.
5726    /// Use `kind` (not this name) to classify the row.
5727    pub name: String,
5728    pub tokens: u64,
5729    /// What the row is: `"used"` content occupies the window; `"free"` is the
5730    /// remaining window; `"buffer"` is the compaction reserve; `"deferred"`
5731    /// rows are out-of-window tool schemas, excluded from usage math.
5732    pub kind: String,
5733}
5734
5735/// An MCP tool's token contribution, in [`ContextUsage::mcp_tools`].
5736#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5737pub struct ContextMcpTool {
5738    /// Wire name, e.g. `"mcp__linear__create_issue"`.
5739    pub name: String,
5740    pub server_name: String,
5741    pub tokens: u64,
5742}
5743
5744/// A memory file's token contribution, in [`ContextUsage::memory_files`].
5745#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5746pub struct ContextMemoryFile {
5747    pub path: String,
5748    /// Display label of the memory-file source, e.g. `"Project"` or `"User"`.
5749    #[serde(rename = "type")]
5750    pub file_type: String,
5751    pub tokens: u64,
5752}
5753
5754/// An agent definition's token contribution, in [`ContextUsage::agents`].
5755#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5756pub struct ContextAgent {
5757    pub agent_type: String,
5758    /// Raw source identifier, e.g. `"projectSettings"`, `"plugin"`.
5759    pub source: String,
5760    pub tokens: u64,
5761}
5762
5763/// A skill's token contribution, in [`ContextUsage::skills`].
5764#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5765pub struct ContextSkill {
5766    pub name: String,
5767    /// Raw source identifier, e.g. `"userSettings"`, `"plugin"`.
5768    pub source: String,
5769    #[serde(default, skip_serializing_if = "Option::is_none")]
5770    pub plugin_name: Option<String>,
5771    pub tokens: u64,
5772}
5773
5774/// Display metadata for a tool-use block carried on the assistant wrapper.
5775#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5776pub struct ToolUseMeta {
5777    pub id: String,
5778    pub display_name: String,
5779    #[serde(default, skip_serializing_if = "Option::is_none")]
5780    pub server_display_name: Option<String>,
5781    #[serde(default, skip_serializing_if = "Option::is_none")]
5782    pub icon_url: Option<String>,
5783    /// The `mcpsrv_` connector id of a server provisioned by claude.ai, set
5784    /// by the CLI from claude.ai's own connector listing, so a client may
5785    /// show the call as that connector's. Not a permission grant (CLI
5786    /// 2.1.300+).
5787    #[serde(default, skip_serializing_if = "Option::is_none")]
5788    pub server_id: Option<String>,
5789    /// The `X-MCP-Server-ID` header value, lower-cased, of an http entry in
5790    /// the dynamic scope (passed at launch or by a later `mcp_set_servers`
5791    /// push). The CLI checks only its shape, not whose it is: look it up in
5792    /// your own server list, treat a miss as an unknown server (never an
5793    /// error), and show a hit as the entry's claim, not proof. Separate from
5794    /// [`server_id`](Self::server_id); both are absent for plugin, repo,
5795    /// settings and agent entries (CLI 2.1.300+).
5796    #[serde(default, skip_serializing_if = "Option::is_none")]
5797    pub server_id_stamp: Option<String>,
5798    /// The tool's name as its server lists it, without the
5799    /// `mcp__<server>__` prefix (CLI 2.1.300+).
5800    #[serde(default, skip_serializing_if = "Option::is_none")]
5801    pub tool_name: Option<String>,
5802    /// `Some(true)` on a call to a publishing tool of an Artifacts MCP entry
5803    /// that reaches only Anthropic's server, which the CLI tells from the
5804    /// entry itself, never its name. The finished call's `tool_use_result`
5805    /// usually carries the artifact's url at its top level (and its title
5806    /// where the server's answer has one), but the mark promises neither,
5807    /// and the url can come on an unmarked call; all are the worker's word,
5808    /// not proof. Sent only on the main loop's own frames: never for a
5809    /// subagent's call or over Remote Control (CLI 2.1.321+).
5810    #[serde(default, skip_serializing_if = "Option::is_none")]
5811    pub artifact_publish: Option<bool>,
5812}
5813
5814/// Assistant message
5815#[derive(Debug, Clone, Serialize, Deserialize)]
5816pub struct AssistantMessage {
5817    pub message: AssistantMessageContent,
5818    #[serde(alias = "sessionId")]
5819    pub session_id: String,
5820    #[serde(skip_serializing_if = "Option::is_none")]
5821    pub uuid: Option<String>,
5822    #[serde(skip_serializing_if = "Option::is_none")]
5823    pub parent_tool_use_id: Option<String>,
5824    /// Anthropic API request id that produced this message (e.g. `req_...`).
5825    #[serde(skip_serializing_if = "Option::is_none")]
5826    pub request_id: Option<String>,
5827    /// Only on the assistant message that reports a refusal: the API `msg_*`
5828    /// id of the refused response (`message.id` on this message is a local
5829    /// UUID). Absent when the CLI has no well-formed id for it, and on CLIs
5830    /// before 2.1.301.
5831    #[serde(default, skip_serializing_if = "Option::is_none")]
5832    pub refused_message_id: Option<String>,
5833    /// Client uuid of the user message that triggered this turn, stamped on
5834    /// the turn's first top-level assistant message so a consumer can bind
5835    /// the reply to the send it answers without waiting for the result. With
5836    /// `--include-partial-messages` the first non-ping stream event is
5837    /// stamped too, independently (CLI 2.1.269+), so the same uuid may
5838    /// appear on both frames. Absent on every later assistant message of the
5839    /// turn, on subagent frames, on synthetic/scheduled (meta) turns, and
5840    /// from CLIs before 2.1.259.
5841    #[serde(default, skip_serializing_if = "Option::is_none")]
5842    pub user_message_uuid: Option<String>,
5843    /// Client uuids of every user message whose prompt this turn has consumed
5844    /// so far, in consumption order — all members of a prompt batch the host
5845    /// merged into this one turn. Always contains `user_message_uuid`; at
5846    /// most 64 entries. Present exactly when `user_message_uuid` is; absent
5847    /// from CLIs before 2.1.259 (fall back to `user_message_uuid`).
5848    #[serde(default, skip_serializing_if = "Vec::is_empty")]
5849    pub user_message_uuids: Vec<String>,
5850    /// ID of the subagent that produced this message: the `task_id` of its
5851    /// task events, unchanged when the subagent is resumed. Absent on
5852    /// main-thread messages (CLI 2.1.292+).
5853    #[serde(default, skip_serializing_if = "Option::is_none")]
5854    pub agent_id: Option<String>,
5855    /// Subagent type, when this assistant message was produced inside a
5856    /// `local_agent` subagent (e.g. `general-purpose`, `Explore`).
5857    #[serde(skip_serializing_if = "Option::is_none")]
5858    pub subagent_type: Option<String>,
5859    /// Short description of the subagent task, present alongside `subagent_type`.
5860    #[serde(skip_serializing_if = "Option::is_none")]
5861    pub task_description: Option<String>,
5862    #[serde(skip_serializing_if = "Option::is_none")]
5863    pub error: Option<AssistantErrorKind>,
5864    /// True when this message was truncated by an interrupt/abort before the
5865    /// stream completed — `stop_reason` was never received and the content
5866    /// may end mid-word. Absent on normally completed messages.
5867    #[serde(default, skip_serializing_if = "Option::is_none")]
5868    pub aborted: Option<bool>,
5869    /// True when this turn continued the preceding truncated assistant turn
5870    /// inside its trailing signed thinking block (max-output-tokens
5871    /// recovery). Histories replayed through the bridge must carry the flag
5872    /// back so the normalizer keeps the run's prefix on the wire.
5873    #[serde(default, skip_serializing_if = "Option::is_none")]
5874    pub resumed_from_incomplete_thinking: Option<bool>,
5875    #[serde(default, skip_serializing_if = "Vec::is_empty")]
5876    pub supersedes: Vec<String>,
5877    #[serde(skip_serializing_if = "Option::is_none")]
5878    pub timestamp: Option<String>,
5879    #[serde(default, skip_serializing_if = "Vec::is_empty")]
5880    pub tool_use_meta: Vec<ToolUseMeta>,
5881    /// `{id, name}` of the original `Batch*` tool_use block(s) for a message
5882    /// whose content was decomposed into synthetic v1 tool_use blocks.
5883    /// Round-tripped so a replayed history reassembles the batch block on the
5884    /// wire. Wrapper-level sibling — never inside `message.content`.
5885    #[serde(default, skip_serializing_if = "Vec::is_empty")]
5886    pub batch_tool_uses: Vec<BatchToolUse>,
5887    /// `tool_use.input` exactly as the API produced it, keyed by `tool_use`
5888    /// id, for a message whose `message.content` carries the
5889    /// client-normalized input. Round-tripped so a replayed history echoes
5890    /// each earlier tool call back to the API as the API emitted it.
5891    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.259+).
5892    #[serde(default, skip_serializing_if = "Option::is_none")]
5893    pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
5894    /// What the client-side input normalization read from process state for
5895    /// the inputs in [`wire_tool_inputs`](Self::wire_tool_inputs), keyed by
5896    /// the same `tool_use` ids. Round-tripped so a replayed history can verify
5897    /// each recorded input against the normalized one. Wrapper-level sibling —
5898    /// never inside `message.content` (CLI 2.1.266+).
5899    #[serde(default, skip_serializing_if = "Option::is_none")]
5900    pub wire_ingest_context: Option<serde_json::Map<String, Value>>,
5901    /// Replayed history rather than a live message, stamped by the Remote
5902    /// Control bridge when it flushes history to the session server, which
5903    /// also stamps it on deliveries it replays (CLI 2.1.266+).
5904    #[serde(default, skip_serializing_if = "Option::is_none")]
5905    pub historical: Option<bool>,
5906    /// Why this frame's turn is the automatic re-run of a turn a worker
5907    /// restart interrupted: the host's `CLAUDE_CODE_RESUME_REASON` when it set
5908    /// one (`host_draining`, `checkpoint_restore`, `container_recreated`,
5909    /// ...), else `interrupted_turn`. Stamped on the same reply frames as
5910    /// `user_message_uuid`, which on such a re-run names the interrupted
5911    /// turn's own last user prompt. Absent on every other turn and from CLIs
5912    /// before 2.1.268.
5913    #[serde(default, skip_serializing_if = "Option::is_none")]
5914    pub resume_reason: Option<String>,
5915    /// The originating `system/local_command` row's wire-form content,
5916    /// carried on the loop-synthesized local-command twin so a bridge/SDK
5917    /// history replay rebuilds the internal system row instead of dropping
5918    /// the output. Wrapper-level sibling — never inside `message.content`
5919    /// (CLI 2.1.259+).
5920    #[serde(default, skip_serializing_if = "Option::is_none")]
5921    pub local_command_source: Option<String>,
5922    /// Ascending zero-based indexes into `message.content` of the thinking
5923    /// blocks whose signature the server tagged as narration (server
5924    /// summaries of the prose between tool calls, not the model's own
5925    /// reasoning), so a renderer can label them as summaries without decoding
5926    /// the signature envelope. Fail-closed: an unparseable or legacy
5927    /// signature is not listed. Omitted when the frame has no such block and
5928    /// by CLIs before 2.1.260; treat unlisted thinking blocks as ordinary
5929    /// thinking. Wrapper-level sibling — never inside `message.content`.
5930    #[serde(default, skip_serializing_if = "Vec::is_empty")]
5931    pub narration_block_indexes: Vec<usize>,
5932    /// How a renderer should treat this frame's narration blocks:
5933    /// `"hidden"` or `"faint"` on CLI 2.1.280+ (kept open for future
5934    /// values). Omitted when the CLI expresses no preference.
5935    #[serde(default, skip_serializing_if = "Option::is_none")]
5936    pub narration_hint: Option<String>,
5937    /// How long the thinking block in this frame took to stream, in whole
5938    /// milliseconds (at least 1): from its `content_block_start` to its
5939    /// `content_block_stop` (or, on CLI 2.1.321+, to the interrupt that cut
5940    /// it), so the wait for the response's first token is not counted. For display only. Sent only on a frame whose one content
5941    /// block is a thinking block that was streamed; absent from CLIs before
5942    /// 2.1.287 and from a CLI replaying history it rebuilt from SDK frames,
5943    /// so be ready to show the block without a duration. Wrapper-level
5944    /// sibling — never inside `message.content`.
5945    #[serde(default, skip_serializing_if = "Option::is_none")]
5946    pub thinking_duration_ms: Option<u64>,
5947    /// The `thinking.display` value Claude Code put in the API request body
5948    /// for the response that streamed this frame's thinking block; same
5949    /// source and meaning as
5950    /// [`StreamEventMessage::thinking_display`](crate::io::StreamEventMessage::thinking_display).
5951    /// Sent only on a frame whose one content block is a thinking block that
5952    /// was streamed, and only when the request body had a display. Absent
5953    /// from CLIs before 2.1.296 and from a CLI replaying history it rebuilt
5954    /// from SDK frames; an absent key states nothing. Marked `@internal`
5955    /// upstream, so treat the value set as open. Wrapper-level sibling —
5956    /// never inside `message.content`.
5957    #[serde(default, skip_serializing_if = "Option::is_none")]
5958    pub thinking_display: Option<String>,
5959    /// Structured twin of the `/context` report, carried on the synthetic
5960    /// assistant message that delivers the markdown table. Present only on
5961    /// `/context` results from CLIs new enough to attach it (2.1.239+).
5962    #[serde(default, skip_serializing_if = "Option::is_none")]
5963    pub context_usage: Option<ContextUsage>,
5964    /// Structured twin of the `/usage` report, carried on the synthetic
5965    /// assistant message that delivers its text: the session totals, the
5966    /// plan's usage rows and extra-usage spend, for remote clients that
5967    /// render a card from data. Present only on `/usage` results from CLIs
5968    /// new enough to attach it (2.1.273+) and from claude.ai-subscriber
5969    /// sessions; the text in `message.content` remains the canonical
5970    /// fallback. Wrapper-level sibling — never inside `message.content`.
5971    #[serde(default, skip_serializing_if = "Option::is_none")]
5972    pub usage_report: Option<Box<UsageReport>>,
5973    /// On the local-command twin, the run that printed the row: the
5974    /// command's name (no slash) and its arguments as the echo shows them
5975    /// (`***` when the command marks them sensitive). Present when a `local`
5976    /// command's dispatch printed the row or a `command.run` hook answered
5977    /// it, absent on a never-ran notice. Wrapper-level sibling — never inside
5978    /// `message.content` (CLI 2.1.273+).
5979    #[serde(default, skip_serializing_if = "Option::is_none")]
5980    pub local_command_run: Option<LocalCommandRun>,
5981    /// On the local-command twin, what the row reports (`unavailable_headless`,
5982    /// `unknown`, `failed`, `restart_required`), so a host can offer a fix
5983    /// instead of relaying the text. Absent when the row reports none of
5984    /// these; new kinds are additive, so treat an unknown one as absent.
5985    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.281+).
5986    #[serde(default, skip_serializing_if = "Option::is_none")]
5987    pub local_command_outcome: Option<LocalCommandOutcome>,
5988    #[serde(default, skip_serializing_if = "Option::is_none")]
5989    pub is_meta: Option<bool>,
5990    #[serde(default, skip_serializing_if = "Option::is_none")]
5991    pub is_virtual: Option<bool>,
5992    #[serde(default, skip_serializing_if = "Option::is_none")]
5993    pub is_api_error_message: Option<bool>,
5994    #[serde(skip_serializing_if = "Option::is_none")]
5995    pub api_error_status: Option<u16>,
5996    /// Typed kind of the API error when `is_api_error_message` is true. An
5997    /// open set the CLI grows release to release: 2.1.274 extends the
5998    /// original `max_output_tokens` / `dlp_request_denied` /
5999    /// `claude_code_version_too_old` with `effort_requires_thinking`,
6000    /// `advisor_incompatible`, `tool_history_mismatch`,
6001    /// `autocompact_thrashing`, `pdf_too_large`, `pdf_password_protected`,
6002    /// `media_removed` (CLI 2.1.288+: the API refused an image or a
6003    /// document, so Claude Code leaves it out of the requests that follow),
6004    /// `no_response`, `tls_untrusted_ca`, `gateway_content_type`,
6005    /// `provider_credentials`, `gateway_signin_required`,
6006    /// `gateway_session_expired`, `api_key_auth_disabled`,
6007    /// `org_disabled_credential`, `invalid_credential_header`,
6008    /// `model_requires_usage_credits`, `long_context_credits_required`,
6009    /// `consent_unanswered`, `no_allowed_fallback`,
6010    /// `model_substitution_disabled`, `field_not_granted`, (CLI 2.1.292+)
6011    /// `usage_limit_reached` — the request was refused by the account's own
6012    /// usage limit, a plan limit or a spend cap on usage credits;
6013    /// [`ApiErrorParams::rate_limit_info`] says which and when it resets —
6014    /// and (CLI 2.1.307+) `empty_completion` — the model ended its turn with
6015    /// no visible text and its one retry had no content block, so the turn
6016    /// ends as an error instead of a blank reply — and (CLI 2.1.311+)
6017    /// `unattended_start_limit` — a cloud session's restarted process hit
6018    /// the configured limit of unattended carry-ons within 24 hours, so no
6019    /// request was sent; terminal for the turn, so a consumer replaying
6020    /// history must not re-send the prompt that led to it — and
6021    /// `unattended_start_unconfirmed` — the process could not check whether
6022    /// it was allowed to carry on, so no request was sent; not terminal —
6023    /// and (CLI 2.1.314+) `stored_login_refused` — the stored claude.ai
6024    /// login was about to go out while the API key helper set by the
6025    /// machine's administrator (or, on 2.1.316+, the organization's
6026    /// settings) isn't in effect or its settings couldn't be confirmed, so
6027    /// no request was sent and the message text says who can clear it —
6028    /// `connection_failed` — Claude Code could not reach the
6029    /// API host or lost the connection (never set for a TLS error code) —
6030    /// `stream_idle_timeout` — Claude Code's own idle timer gave up waiting
6031    /// on an open stream, which does not by itself mean the server stalled —
6032    /// and `computer_slept` — the machine slept through the wait for the
6033    /// response (never sent by a claude-code-server session) — and
6034    /// (CLI 2.1.317+) `malformed_tool_use_exhausted` — the model's reply
6035    /// stopped for a tool call and held none, and the loop's retries ran
6036    /// out; terminal for the turn, so a consumer replaying history must not
6037    /// re-send the prompt by itself — and `network_edge_blocked` — the
6038    /// firewall in front of Anthropic's API refused the request for its
6039    /// network location (an unsupported region, or a VPN, proxy or network
6040    /// routing through one) before it reached the API; no sign-in or
6041    /// credential change fixes it — and (CLI 2.1.321+) `hook_denied` — a
6042    /// mod's `http.fetch` hook stopped the request.
6043    /// Kinds with parameters carry them in [`Self::api_error_params`].
6044    #[serde(skip_serializing_if = "Option::is_none")]
6045    pub api_error: Option<String>,
6046    /// The server's `error.details.error_code` for this API error, copied
6047    /// through when it is an identifier (`^[a-z][a-z0-9_]{0,63}$`) and
6048    /// dropped otherwise. Carries server gate codes this CLI build has no
6049    /// `api_error` value for, so a host can key on a new gate without a
6050    /// Claude Code release. Absent when the response carried no code and
6051    /// from CLIs before 2.1.274.
6052    #[serde(default, skip_serializing_if = "Option::is_none")]
6053    pub api_error_code: Option<String>,
6054    /// `first_party` when this API error was built from a response
6055    /// Anthropic's own API sent: the first-party provider, the default
6056    /// `https://api.anthropic.com` endpoint answering directly, over TLS
6057    /// verified with no added CA, override or socket tunnel. Absent in every
6058    /// other case (a base-URL override, a gateway, a third-party provider, a
6059    /// connection error, an error Claude Code raised itself, any replayed
6060    /// frame); treat absence as unknown origin. It says which endpoint
6061    /// answered, not that the text is the server's verbatim, so render
6062    /// stamped text as plain text. Open set — treat an unknown value as
6063    /// absent (CLI 2.1.300+).
6064    #[serde(default, skip_serializing_if = "Option::is_none")]
6065    pub api_error_origin: Option<String>,
6066    /// Parameters of [`Self::api_error`], present only for the kinds that
6067    /// have any (CLI 2.1.274+).
6068    #[serde(default, skip_serializing_if = "Option::is_none")]
6069    pub api_error_params: Option<ApiErrorParams>,
6070    #[serde(skip_serializing_if = "Option::is_none")]
6071    pub error_details: Option<String>,
6072    #[serde(skip_serializing_if = "Option::is_none")]
6073    pub advisor_model: Option<String>,
6074    #[serde(skip_serializing_if = "Option::is_none")]
6075    pub attribution_agent: Option<String>,
6076    #[serde(skip_serializing_if = "Option::is_none")]
6077    pub attribution_skill: Option<String>,
6078    #[serde(skip_serializing_if = "Option::is_none")]
6079    pub attribution_plugin: Option<String>,
6080    #[serde(skip_serializing_if = "Option::is_none")]
6081    pub attribution_mcp_server: Option<String>,
6082    #[serde(skip_serializing_if = "Option::is_none")]
6083    pub attribution_mcp_tool: Option<String>,
6084}
6085
6086/// Parameters of an assistant frame's `api_error`, carried as
6087/// [`AssistantMessage::api_error_params`] only for the error kinds that have
6088/// any (CLI 2.1.274+).
6089#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
6090pub struct ApiErrorParams {
6091    /// The effort level the API refused (`effort_requires_thinking`).
6092    #[serde(default, skip_serializing_if = "Option::is_none")]
6093    pub effort: Option<String>,
6094    /// The API provider whose credentials failed (`provider_credentials`,
6095    /// `gateway_session_expired`).
6096    #[serde(default, skip_serializing_if = "Option::is_none")]
6097    pub provider: Option<ApiErrorProvider>,
6098    /// How to repair the failed credentials (`provider_credentials`,
6099    /// `gateway_session_expired`).
6100    #[serde(default, skip_serializing_if = "Option::is_none")]
6101    pub remedy: Option<ApiErrorRemedy>,
6102    /// The kind of block the API refused (`media_removed`); absent for
6103    /// [`ApiErrorMediaReason::MediaBudget`] (CLI 2.1.288+).
6104    #[serde(default, skip_serializing_if = "Option::is_none")]
6105    pub media: Option<ApiErrorMedia>,
6106    /// Why the API refused the block (`media_removed`, CLI 2.1.288+).
6107    #[serde(default, skip_serializing_if = "Option::is_none")]
6108    pub media_reason: Option<ApiErrorMediaReason>,
6109    /// What the refused request's own response said about the limit
6110    /// (`usage_limit_reached`), in the shape `rate_limit_event` carries:
6111    /// `status` is rejected and `isUsingOverage` false, `rateLimitType` names
6112    /// the limit, `resetsAt` is when it resets, and the overage fields and
6113    /// `limitScope` say whose spend cap it is. It is that refusal's record
6114    /// and is never updated, so compare `resetsAt` with the clock before
6115    /// calling the limit current (CLI 2.1.292+).
6116    ///
6117    /// On CLI 2.1.316+ also carried for `model_requires_usage_credits`:
6118    /// `status` is rejected and `isUsingOverage` false; `rateLimitType` is
6119    /// the response's claim (`seven_day_overage_included` when an included
6120    /// allowance was used up) and absent when it named none; `resetsAt`,
6121    /// `overageStatus`, `overageResetsAt` and `limitScope` come when the
6122    /// response had them; `errorCode` is `credits_required` when the body
6123    /// said so; `overageDisabledReason`, `canUserPurchaseCredits` and
6124    /// `hasChargeableSavedPaymentMethod` as the response gave them; the
6125    /// person's own cap arrives as `org_level_disabled_until`. No other
6126    /// member is sent.
6127    ///
6128    /// On CLI 2.1.319+ the `usage_limit_reached` record also carries
6129    /// `dailyCapPaused` when usage is paused for the organization.
6130    #[serde(default, skip_serializing_if = "Option::is_none")]
6131    pub rate_limit_info: Option<RateLimitInfo>,
6132    /// Which API stop reason cut the reply short (`max_output_tokens`),
6133    /// which the kind alone does not say. Absent on the one other error
6134    /// that carries `max_output_tokens`, the API's `tool_use_block_cut_off`,
6135    /// which [`AssistantMessage::api_error_code`] names (CLI 2.1.321+).
6136    #[serde(default, skip_serializing_if = "Option::is_none")]
6137    pub cut: Option<ApiErrorCut>,
6138}
6139
6140/// Which API stop reason cut a reply short ([`ApiErrorParams::cut`]).
6141#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6142pub enum ApiErrorCut {
6143    /// `max_tokens`: the request's output-token maximum.
6144    OutputLimit,
6145    /// `model_context_window_exceeded`: the model's context window.
6146    ContextWindow,
6147    /// A value not yet known to this version of the crate.
6148    Unknown(String),
6149}
6150
6151impl ApiErrorCut {
6152    pub fn as_str(&self) -> &str {
6153        match self {
6154            Self::OutputLimit => "output_limit",
6155            Self::ContextWindow => "context_window",
6156            Self::Unknown(s) => s.as_str(),
6157        }
6158    }
6159}
6160
6161impl fmt::Display for ApiErrorCut {
6162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6163        f.write_str(self.as_str())
6164    }
6165}
6166
6167impl From<&str> for ApiErrorCut {
6168    fn from(s: &str) -> Self {
6169        match s {
6170            "output_limit" => Self::OutputLimit,
6171            "context_window" => Self::ContextWindow,
6172            other => Self::Unknown(other.to_string()),
6173        }
6174    }
6175}
6176
6177impl Serialize for ApiErrorCut {
6178    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
6179        serializer.serialize_str(self.as_str())
6180    }
6181}
6182
6183impl<'de> Deserialize<'de> for ApiErrorCut {
6184    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
6185        let s = String::deserialize(deserializer)?;
6186        Ok(Self::from(s.as_str()))
6187    }
6188}
6189
6190/// The kind of block the API refused ([`ApiErrorParams::media`]).
6191#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6192pub enum ApiErrorMedia {
6193    Image,
6194    Document,
6195    /// A kind not yet known to this version of the crate.
6196    Unknown(String),
6197}
6198
6199impl ApiErrorMedia {
6200    pub fn as_str(&self) -> &str {
6201        match self {
6202            Self::Image => "image",
6203            Self::Document => "document",
6204            Self::Unknown(s) => s.as_str(),
6205        }
6206    }
6207}
6208
6209impl fmt::Display for ApiErrorMedia {
6210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6211        f.write_str(self.as_str())
6212    }
6213}
6214
6215impl From<&str> for ApiErrorMedia {
6216    fn from(s: &str) -> Self {
6217        match s {
6218            "image" => Self::Image,
6219            "document" => Self::Document,
6220            other => Self::Unknown(other.to_string()),
6221        }
6222    }
6223}
6224
6225impl Serialize for ApiErrorMedia {
6226    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
6227        serializer.serialize_str(self.as_str())
6228    }
6229}
6230
6231impl<'de> Deserialize<'de> for ApiErrorMedia {
6232    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
6233        let s = String::deserialize(deserializer)?;
6234        Ok(Self::from(s.as_str()))
6235    }
6236}
6237
6238/// Why the API refused an image or document
6239/// ([`ApiErrorParams::media_reason`]).
6240#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6241pub enum ApiErrorMediaReason {
6242    /// Every other refusal, a gateway's report of a model's refusal among
6243    /// them.
6244    Unprocessable,
6245    /// The API said that the model does not accept PDF documents.
6246    UnsupportedByModel,
6247    /// The request's images and documents together exceed the API's limit,
6248    /// and the requests that follow may leave out blocks of either kind.
6249    MediaBudget,
6250    /// A reason not yet known to this version of the crate.
6251    Unknown(String),
6252}
6253
6254impl ApiErrorMediaReason {
6255    pub fn as_str(&self) -> &str {
6256        match self {
6257            Self::Unprocessable => "unprocessable",
6258            Self::UnsupportedByModel => "unsupported_by_model",
6259            Self::MediaBudget => "media_budget",
6260            Self::Unknown(s) => s.as_str(),
6261        }
6262    }
6263}
6264
6265impl fmt::Display for ApiErrorMediaReason {
6266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6267        f.write_str(self.as_str())
6268    }
6269}
6270
6271impl From<&str> for ApiErrorMediaReason {
6272    fn from(s: &str) -> Self {
6273        match s {
6274            "unprocessable" => Self::Unprocessable,
6275            "unsupported_by_model" => Self::UnsupportedByModel,
6276            "media_budget" => Self::MediaBudget,
6277            other => Self::Unknown(other.to_string()),
6278        }
6279    }
6280}
6281
6282impl Serialize for ApiErrorMediaReason {
6283    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
6284        serializer.serialize_str(self.as_str())
6285    }
6286}
6287
6288impl<'de> Deserialize<'de> for ApiErrorMediaReason {
6289    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
6290        let s = String::deserialize(deserializer)?;
6291        Ok(Self::from(s.as_str()))
6292    }
6293}
6294
6295/// The API provider whose credentials failed ([`ApiErrorParams::provider`]).
6296#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6297pub enum ApiErrorProvider {
6298    Bedrock,
6299    AnthropicAws,
6300    Mantle,
6301    AnthropicGoogleCloud,
6302    Vertex,
6303    Foundry,
6304    Gateway,
6305    /// A provider not yet known to this version of the crate.
6306    Unknown(String),
6307}
6308
6309impl ApiErrorProvider {
6310    pub fn as_str(&self) -> &str {
6311        match self {
6312            Self::Bedrock => "bedrock",
6313            Self::AnthropicAws => "anthropicAws",
6314            Self::Mantle => "mantle",
6315            Self::AnthropicGoogleCloud => "anthropicGoogleCloud",
6316            Self::Vertex => "vertex",
6317            Self::Foundry => "foundry",
6318            Self::Gateway => "gateway",
6319            Self::Unknown(s) => s.as_str(),
6320        }
6321    }
6322}
6323
6324impl fmt::Display for ApiErrorProvider {
6325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6326        f.write_str(self.as_str())
6327    }
6328}
6329
6330impl From<&str> for ApiErrorProvider {
6331    fn from(s: &str) -> Self {
6332        match s {
6333            "bedrock" => Self::Bedrock,
6334            "anthropicAws" => Self::AnthropicAws,
6335            "mantle" => Self::Mantle,
6336            "anthropicGoogleCloud" => Self::AnthropicGoogleCloud,
6337            "vertex" => Self::Vertex,
6338            "foundry" => Self::Foundry,
6339            "gateway" => Self::Gateway,
6340            other => Self::Unknown(other.to_string()),
6341        }
6342    }
6343}
6344
6345impl Serialize for ApiErrorProvider {
6346    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
6347        serializer.serialize_str(self.as_str())
6348    }
6349}
6350
6351impl<'de> Deserialize<'de> for ApiErrorProvider {
6352    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
6353        let s = String::deserialize(deserializer)?;
6354        Ok(Self::from(s.as_str()))
6355    }
6356}
6357
6358/// How to repair failed provider credentials ([`ApiErrorParams::remedy`]).
6359#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6360pub enum ApiErrorRemedy {
6361    /// A configured refresh command (`awsAuthRefresh` / `gcpAuthRefresh`)
6362    /// refreshes the credentials.
6363    RefreshCommand,
6364    /// Refresh the provider's credentials by hand; no command is configured.
6365    RefreshCredentials,
6366    /// Refresh the Google application default credentials or the
6367    /// `GOOGLE_APPLICATION_CREDENTIALS` key file.
6368    Adc,
6369    /// Replace the gateway token supplied through `ANTHROPIC_AUTH_TOKEN` or
6370    /// `ANTHROPIC_CUSTOM_HEADERS`.
6371    GatewayToken,
6372    /// The host application owns these credentials.
6373    HostManaged,
6374    /// The credentials work but the model is not enabled for this account
6375    /// and region (Amazon Bedrock).
6376    ModelAccess,
6377    /// A remedy not yet known to this version of the crate.
6378    Unknown(String),
6379}
6380
6381impl ApiErrorRemedy {
6382    pub fn as_str(&self) -> &str {
6383        match self {
6384            Self::RefreshCommand => "refresh_command",
6385            Self::RefreshCredentials => "refresh_credentials",
6386            Self::Adc => "adc",
6387            Self::GatewayToken => "gateway_token",
6388            Self::HostManaged => "host_managed",
6389            Self::ModelAccess => "model_access",
6390            Self::Unknown(s) => s.as_str(),
6391        }
6392    }
6393}
6394
6395impl fmt::Display for ApiErrorRemedy {
6396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6397        f.write_str(self.as_str())
6398    }
6399}
6400
6401impl From<&str> for ApiErrorRemedy {
6402    fn from(s: &str) -> Self {
6403        match s {
6404            "refresh_command" => Self::RefreshCommand,
6405            "refresh_credentials" => Self::RefreshCredentials,
6406            "adc" => Self::Adc,
6407            "gateway_token" => Self::GatewayToken,
6408            "host_managed" => Self::HostManaged,
6409            "model_access" => Self::ModelAccess,
6410            other => Self::Unknown(other.to_string()),
6411        }
6412    }
6413}
6414
6415impl Serialize for ApiErrorRemedy {
6416    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
6417        serializer.serialize_str(self.as_str())
6418    }
6419}
6420
6421impl<'de> Deserialize<'de> for ApiErrorRemedy {
6422    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
6423        let s = String::deserialize(deserializer)?;
6424        Ok(Self::from(s.as_str()))
6425    }
6426}
6427
6428/// Nested message content for assistant messages
6429#[derive(Debug, Clone, Serialize, Deserialize)]
6430pub struct AssistantMessageContent {
6431    pub id: String,
6432    /// The Anthropic API message type — always `"message"`.
6433    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
6434    pub message_type: Option<String>,
6435    pub role: MessageRole,
6436    pub model: String,
6437    pub content: Vec<ContentBlock>,
6438    #[serde(skip_serializing_if = "Option::is_none")]
6439    pub stop_reason: Option<StopReason>,
6440    #[serde(skip_serializing_if = "Option::is_none")]
6441    pub stop_sequence: Option<String>,
6442    #[serde(skip_serializing_if = "Option::is_none")]
6443    pub usage: Option<AssistantUsage>,
6444    /// Details about why generation stopped
6445    #[serde(skip_serializing_if = "Option::is_none")]
6446    pub stop_details: Option<Value>,
6447    /// Context management metadata
6448    #[serde(skip_serializing_if = "Option::is_none")]
6449    pub context_management: Option<Value>,
6450}
6451
6452/// Usage information for assistant messages
6453#[derive(Debug, Clone, Serialize, Deserialize)]
6454pub struct AssistantUsage {
6455    /// Number of input tokens
6456    #[serde(default)]
6457    pub input_tokens: u32,
6458
6459    /// Number of output tokens
6460    #[serde(default)]
6461    pub output_tokens: u32,
6462
6463    /// Tokens used to create cache
6464    #[serde(default)]
6465    pub cache_creation_input_tokens: u32,
6466
6467    /// Tokens read from cache
6468    #[serde(default)]
6469    pub cache_read_input_tokens: u32,
6470
6471    /// Service tier used (e.g., "standard")
6472    #[serde(skip_serializing_if = "Option::is_none")]
6473    pub service_tier: Option<String>,
6474
6475    /// Detailed cache creation breakdown
6476    #[serde(skip_serializing_if = "Option::is_none")]
6477    pub cache_creation: Option<CacheCreationDetails>,
6478
6479    /// Inference geography (e.g., "not_available")
6480    #[serde(skip_serializing_if = "Option::is_none")]
6481    pub inference_geo: Option<String>,
6482
6483    /// Speed tier the request ran at (`"standard"` or `"fast"`). The API
6484    /// began sending it on per-message usage around CLI 2.1.301.
6485    #[serde(default, skip_serializing_if = "Option::is_none")]
6486    pub speed: Option<String>,
6487
6488    /// Outcome of a fallback-credit token the request presented; `null` on
6489    /// the wire when it presented none (CLI 2.1.285+).
6490    #[serde(default, skip_serializing_if = "Option::is_none")]
6491    pub fallback_credit: Option<Value>,
6492}
6493
6494/// Detailed cache creation information
6495#[derive(Debug, Clone, Serialize, Deserialize)]
6496pub struct CacheCreationDetails {
6497    /// Ephemeral 1-hour input tokens
6498    #[serde(default)]
6499    pub ephemeral_1h_input_tokens: u32,
6500
6501    /// Ephemeral 5-minute input tokens
6502    #[serde(default)]
6503    pub ephemeral_5m_input_tokens: u32,
6504}
6505
6506#[cfg(test)]
6507mod tests {
6508    use crate::io::ClaudeOutput;
6509
6510    #[test]
6511    fn test_subagent_usage_rollup_accumulates_task_results() {
6512        use super::SubagentUsageRollup;
6513
6514        let mut rollup = SubagentUsageRollup::default();
6515
6516        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}}"#;
6517        let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
6518        assert!(rollup.observe(&output));
6519        assert_eq!(rollup.subagent_tokens, 10201);
6520        assert_eq!(rollup.agent_count, 1);
6521        assert_eq!(rollup.tool_uses, 3);
6522        assert_eq!(rollup.duration_ms, 1853);
6523
6524        // Replayed frame with the same agentId is counted once.
6525        assert!(!rollup.observe(&output));
6526        assert_eq!(rollup.agent_count, 1);
6527        assert_eq!(rollup.subagent_tokens, 10201);
6528
6529        // A second agent accumulates.
6530        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}}"#;
6531        let output: ClaudeOutput = serde_json::from_str(second).unwrap();
6532        assert!(rollup.observe(&output));
6533        assert_eq!(rollup.agent_count, 2);
6534        assert_eq!(rollup.subagent_tokens, 10701);
6535    }
6536
6537    #[test]
6538    fn test_subagent_usage_rollup_ignores_non_task_results() {
6539        use super::SubagentUsageRollup;
6540
6541        let mut rollup = SubagentUsageRollup::default();
6542
6543        // A ToolSearch tool_use_result parses as an all-None SubagentResult;
6544        // it must not count as a subagent.
6545        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}}"#;
6546        let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
6547        assert!(!rollup.observe(&output));
6548
6549        // Plain user message without tool_use_result.
6550        let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
6551        let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
6552        assert!(!rollup.observe(&output));
6553
6554        // Non-user frames are ignored.
6555        let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
6556        let output: ClaudeOutput = serde_json::from_str(system).unwrap();
6557        assert!(!rollup.observe(&output));
6558
6559        assert_eq!(rollup, SubagentUsageRollup::default());
6560    }
6561
6562    #[test]
6563    fn test_subagent_usage_rollup_over_captured_session() {
6564        use super::SubagentUsageRollup;
6565
6566        let mut rollup = SubagentUsageRollup::default();
6567        let fixture =
6568            include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
6569        for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
6570            if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
6571                rollup.observe(&output);
6572            }
6573        }
6574        assert_eq!(rollup.agent_count, 1);
6575        assert_eq!(rollup.subagent_tokens, 10201);
6576    }
6577
6578    #[test]
6579    fn test_system_message_init() {
6580        let json = r#"{
6581            "type": "system",
6582            "subtype": "init",
6583            "session_id": "test-session-123",
6584            "cwd": "/home/user/project",
6585            "model": "claude-sonnet-4",
6586            "tools": ["Bash", "Read", "Write"],
6587            "mcp_servers": [],
6588            "slash_commands": ["compact", "cost", "review"],
6589            "agents": ["Bash", "Explore", "Plan"],
6590            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
6591            "skills": [],
6592            "claude_code_version": "2.1.15",
6593            "apiKeySource": "none",
6594            "output_style": "default",
6595            "permissionMode": "default"
6596        }"#;
6597
6598        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6599        if let ClaudeOutput::System(sys) = output {
6600            assert!(sys.is_init());
6601            assert!(!sys.is_status());
6602            assert!(!sys.is_compact_boundary());
6603
6604            let init = sys.as_init().expect("Should parse as init");
6605            assert_eq!(init.session_id, "test-session-123");
6606            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
6607            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
6608            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
6609            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
6610            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
6611            assert_eq!(init.plugins.len(), 1);
6612            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
6613            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
6614            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
6615            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
6616            assert_eq!(
6617                init.permission_mode,
6618                Some(super::InitPermissionMode::Default)
6619            );
6620        } else {
6621            panic!("Expected System message");
6622        }
6623    }
6624
6625    #[test]
6626    fn test_system_message_init_from_real_capture() {
6627        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
6628        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6629        if let ClaudeOutput::System(sys) = output {
6630            let init = sys.as_init().expect("Should parse real init capture");
6631            assert_eq!(init.slash_commands.len(), 8);
6632            assert!(init.slash_commands.contains(&"compact".to_string()));
6633            assert!(init.slash_commands.contains(&"review".to_string()));
6634            assert_eq!(init.agents.len(), 5);
6635            assert!(init.agents.contains(&"Bash".to_string()));
6636            assert!(init.agents.contains(&"Explore".to_string()));
6637            assert_eq!(init.plugins.len(), 1);
6638            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
6639            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
6640        } else {
6641            panic!("Expected System message");
6642        }
6643    }
6644
6645    #[test]
6646    fn test_system_message_status() {
6647        let json = r#"{
6648            "type": "system",
6649            "subtype": "status",
6650            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
6651            "status": "compacting",
6652            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
6653        }"#;
6654
6655        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6656        if let ClaudeOutput::System(sys) = output {
6657            assert!(sys.is_status());
6658            assert!(!sys.is_init());
6659
6660            let status = sys.as_status().expect("Should parse as status");
6661            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
6662            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
6663            assert_eq!(
6664                status.uuid,
6665                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
6666            );
6667        } else {
6668            panic!("Expected System message");
6669        }
6670    }
6671
6672    #[test]
6673    fn test_system_message_status_null() {
6674        let json = r#"{
6675            "type": "system",
6676            "subtype": "status",
6677            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
6678            "status": null,
6679            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
6680        }"#;
6681
6682        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6683        if let ClaudeOutput::System(sys) = output {
6684            let status = sys.as_status().expect("Should parse as status");
6685            assert_eq!(status.status, None);
6686        } else {
6687            panic!("Expected System message");
6688        }
6689    }
6690
6691    #[test]
6692    fn test_system_message_task_started() {
6693        let json = r#"{
6694            "type": "system",
6695            "subtype": "task_started",
6696            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
6697            "task_id": "b6daf3f",
6698            "task_type": "local_bash",
6699            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
6700            "description": "Wait for CI on PR #12",
6701            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
6702        }"#;
6703
6704        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6705        if let ClaudeOutput::System(sys) = output {
6706            assert!(sys.is_task_started());
6707            assert!(!sys.is_task_progress());
6708            assert!(!sys.is_task_notification());
6709
6710            let task = sys.as_task_started().expect("Should parse as task_started");
6711            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
6712            assert_eq!(task.task_id, "b6daf3f");
6713            assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
6714            assert_eq!(
6715                task.tool_use_id.as_deref(),
6716                Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
6717            );
6718            assert_eq!(task.description, "Wait for CI on PR #12");
6719        } else {
6720            panic!("Expected System message");
6721        }
6722    }
6723
6724    #[test]
6725    fn test_system_message_task_started_agent() {
6726        let json = r#"{
6727            "type": "system",
6728            "subtype": "task_started",
6729            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
6730            "task_id": "a4a7e0906e5fc64cc",
6731            "task_type": "local_agent",
6732            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
6733            "description": "Explore Scene/ArrayScene duplication",
6734            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
6735        }"#;
6736
6737        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6738        if let ClaudeOutput::System(sys) = output {
6739            let task = sys.as_task_started().expect("Should parse as task_started");
6740            assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
6741            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
6742        } else {
6743            panic!("Expected System message");
6744        }
6745    }
6746
6747    #[test]
6748    fn test_system_message_task_progress() {
6749        let json = r#"{
6750            "type": "system",
6751            "subtype": "task_progress",
6752            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
6753            "task_id": "a4a7e0906e5fc64cc",
6754            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
6755            "description": "Reading src/jplephem/chebyshev.rs",
6756            "last_tool_name": "Read",
6757            "usage": {
6758                "duration_ms": 13996,
6759                "tool_uses": 9,
6760                "total_tokens": 38779
6761            },
6762            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
6763        }"#;
6764
6765        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6766        if let ClaudeOutput::System(sys) = output {
6767            assert!(sys.is_task_progress());
6768            assert!(!sys.is_task_started());
6769
6770            let progress = sys
6771                .as_task_progress()
6772                .expect("Should parse as task_progress");
6773            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
6774            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
6775            assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
6776            assert_eq!(progress.usage.duration_ms, 13996);
6777            assert_eq!(progress.usage.tool_uses, 9);
6778            assert_eq!(progress.usage.total_tokens, 38779);
6779        } else {
6780            panic!("Expected System message");
6781        }
6782    }
6783
6784    #[test]
6785    fn test_system_message_task_notification_completed() {
6786        let json = r#"{
6787            "type": "system",
6788            "subtype": "task_notification",
6789            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
6790            "task_id": "a0ba761e9dc9c316f",
6791            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
6792            "status": "completed",
6793            "summary": "Agent \"Write Hipparcos data source doc\" completed",
6794            "output_file": "",
6795            "usage": {
6796                "duration_ms": 172300,
6797                "tool_uses": 11,
6798                "total_tokens": 42005
6799            },
6800            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
6801        }"#;
6802
6803        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6804        if let ClaudeOutput::System(sys) = output {
6805            assert!(sys.is_task_notification());
6806
6807            let notif = sys
6808                .as_task_notification()
6809                .expect("Should parse as task_notification");
6810            assert_eq!(notif.status, super::TaskStatus::Completed);
6811            assert_eq!(
6812                notif.summary,
6813                "Agent \"Write Hipparcos data source doc\" completed"
6814            );
6815            assert_eq!(notif.output_file, Some("".to_string()));
6816            assert_eq!(
6817                notif.tool_use_id,
6818                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
6819            );
6820            let usage = notif.usage.expect("Should have usage");
6821            assert_eq!(usage.duration_ms, 172300);
6822            assert_eq!(usage.tool_uses, 11);
6823            assert_eq!(usage.total_tokens, 42005);
6824        } else {
6825            panic!("Expected System message");
6826        }
6827    }
6828
6829    #[test]
6830    fn test_system_message_task_notification_failed_no_usage() {
6831        let json = r#"{
6832            "type": "system",
6833            "subtype": "task_notification",
6834            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
6835            "task_id": "b98f6a3",
6836            "status": "failed",
6837            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
6838            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
6839        }"#;
6840
6841        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6842        if let ClaudeOutput::System(sys) = output {
6843            let notif = sys
6844                .as_task_notification()
6845                .expect("Should parse as task_notification");
6846            assert_eq!(notif.status, super::TaskStatus::Failed);
6847            assert!(notif.tool_use_id.is_none());
6848            assert!(notif.usage.is_none());
6849            assert_eq!(
6850                notif.output_file,
6851                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
6852            );
6853        } else {
6854            panic!("Expected System message");
6855        }
6856    }
6857
6858    /// Task system messages survive a `to_value` → `from_value` round-trip
6859    /// with their typed accessors still resolving. Mirrors the proxy/relay
6860    /// path where output is reparsed from a `serde_json::Value` rather than
6861    /// straight from the CLI's stdout, so a silently dropped or renamed field
6862    /// surfaces here instead of as a `None` downstream.
6863    #[test]
6864    fn test_task_messages_roundtrip_through_value() {
6865        let cases = [
6866            r#"{"type":"system","subtype":"task_started","session_id":"s1",
6867                "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
6868                "description":"Sleep 3s","uuid":"u1"}"#,
6869            r#"{"type":"system","subtype":"task_progress","session_id":"s1",
6870                "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
6871                "last_tool_name":"Bash",
6872                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
6873                "uuid":"u2"}"#,
6874            r#"{"type":"system","subtype":"task_notification","session_id":"s1",
6875                "task_id":"t1","tool_use_id":"tu1","status":"completed",
6876                "summary":"done","output_file":"",
6877                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
6878                "uuid":"u3"}"#,
6879        ];
6880
6881        for json in cases {
6882            let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6883            let value = serde_json::to_value(&output).unwrap();
6884            let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
6885
6886            let ClaudeOutput::System(sys) = reparsed else {
6887                panic!("Expected System variant after round-trip");
6888            };
6889
6890            match sys.subtype {
6891                super::SystemSubtype::TaskStarted => {
6892                    assert!(
6893                        sys.as_task_started().is_some(),
6894                        "as_task_started failed after round-trip"
6895                    );
6896                }
6897                super::SystemSubtype::TaskProgress => {
6898                    assert!(
6899                        sys.as_task_progress().is_some(),
6900                        "as_task_progress failed after round-trip"
6901                    );
6902                }
6903                super::SystemSubtype::TaskNotification => {
6904                    assert!(
6905                        sys.as_task_notification().is_some(),
6906                        "as_task_notification failed after round-trip"
6907                    );
6908                }
6909                other => panic!("unexpected subtype after round-trip: {other:?}"),
6910            }
6911        }
6912    }
6913
6914    #[test]
6915    fn test_system_message_compact_boundary() {
6916        let json = r#"{
6917            "type": "system",
6918            "subtype": "compact_boundary",
6919            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
6920            "compact_metadata": {
6921                "pre_tokens": 155285,
6922                "trigger": "auto"
6923            },
6924            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
6925        }"#;
6926
6927        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6928        if let ClaudeOutput::System(sys) = output {
6929            assert!(sys.is_compact_boundary());
6930            assert!(!sys.is_init());
6931            assert!(!sys.is_status());
6932
6933            let compact = sys
6934                .as_compact_boundary()
6935                .expect("Should parse as compact_boundary");
6936            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
6937            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
6938            assert_eq!(
6939                compact.compact_metadata.trigger,
6940                super::CompactionTrigger::Auto
6941            );
6942            // Per-compaction stats are optional and absent here.
6943            assert!(compact.summary.is_none());
6944            assert!(compact.leaf_message_count.is_none());
6945            assert!(compact.duration_ms.is_none());
6946        } else {
6947            panic!("Expected System message");
6948        }
6949    }
6950
6951    #[test]
6952    fn test_compact_boundary_with_summary_stats() {
6953        // Canonical keys.
6954        let json = r#"{
6955            "type": "system",
6956            "subtype": "compact_boundary",
6957            "session_id": "s1",
6958            "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
6959            "summary": "Summarized the earlier exploration.",
6960            "leaf_message_count": 42,
6961            "duration_ms": 1234,
6962            "uuid": "u1"
6963        }"#;
6964        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6965        let ClaudeOutput::System(sys) = output else {
6966            panic!("Expected System message");
6967        };
6968        let compact = sys.as_compact_boundary().expect("compact_boundary");
6969        assert_eq!(
6970            compact.summary.as_deref(),
6971            Some("Summarized the earlier exploration.")
6972        );
6973        assert_eq!(compact.leaf_message_count, Some(42));
6974        assert_eq!(compact.duration_ms, Some(1234));
6975
6976        // Alternate wire keys (`content` for summary, `message_count` for count)
6977        // deserialize into the same fields.
6978        let json_alt = r#"{
6979            "type": "system",
6980            "subtype": "compact_boundary",
6981            "session_id": "s2",
6982            "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
6983            "content": "alt-key summary",
6984            "message_count": 7
6985        }"#;
6986        let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
6987        let ClaudeOutput::System(sys) = output else {
6988            panic!("Expected System message");
6989        };
6990        let compact = sys.as_compact_boundary().expect("compact_boundary");
6991        assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
6992        assert_eq!(compact.leaf_message_count, Some(7));
6993    }
6994
6995    #[test]
6996    fn test_init_message_with_new_fields() {
6997        let json = r#"{
6998            "type": "system",
6999            "subtype": "init",
7000            "session_id": "test-session",
7001            "cwd": "/home/user",
7002            "model": "claude-opus-4-7",
7003            "tools": ["Bash"],
7004            "mcp_servers": [],
7005            "permissionMode": "default",
7006            "apiKeySource": "none",
7007            "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
7008            "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
7009            "fast_mode_state": "off",
7010            "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
7011            "claude_code_version": "2.1.117"
7012        }"#;
7013
7014        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7015        if let ClaudeOutput::System(sys) = output {
7016            let init = sys.as_init().expect("Should parse as init");
7017            assert_eq!(
7018                init.uuid.as_deref(),
7019                Some("44841a0d-182d-493a-86b5-79800d3d9665")
7020            );
7021            assert!(init.memory_paths.is_some());
7022            assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
7023            assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
7024            assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
7025        } else {
7026            panic!("Expected System message");
7027        }
7028    }
7029
7030    #[test]
7031    fn test_assistant_message_with_new_fields() {
7032        let json = r#"{
7033            "type": "assistant",
7034            "message": {
7035                "id": "msg_1",
7036                "type": "message",
7037                "role": "assistant",
7038                "model": "claude-opus-4-7",
7039                "content": [{"type": "text", "text": "Hello"}],
7040                "stop_reason": "end_turn",
7041                "stop_details": null,
7042                "context_management": null,
7043                "usage": {
7044                    "input_tokens": 100,
7045                    "output_tokens": 10,
7046                    "cache_creation_input_tokens": 50,
7047                    "cache_read_input_tokens": 0,
7048                    "service_tier": "standard",
7049                    "inference_geo": "not_available"
7050                }
7051            },
7052            "session_id": "abc",
7053            "uuid": "msg-uuid-123"
7054        }"#;
7055
7056        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7057        if let ClaudeOutput::Assistant(asst) = output {
7058            assert_eq!(asst.message.stop_details, None);
7059            assert_eq!(asst.message.context_management, None);
7060            let usage = asst.message.usage.unwrap();
7061            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
7062        } else {
7063            panic!("Expected Assistant message");
7064        }
7065    }
7066
7067    #[test]
7068    fn test_user_message_with_new_fields() {
7069        let json = r#"{
7070            "type": "user",
7071            "message": {
7072                "role": "user",
7073                "content": [{"type": "text", "text": "Hello"}]
7074            },
7075            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
7076            "parent_tool_use_id": "toolu_123",
7077            "uuid": "user-msg-456"
7078        }"#;
7079
7080        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7081        if let ClaudeOutput::User(user) = output {
7082            assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
7083            assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
7084        } else {
7085            panic!("Expected User message");
7086        }
7087    }
7088
7089    /// Real wire payload captured from the CLI after answering an
7090    /// AskUserQuestion via the permission control protocol. The top-level
7091    /// `tool_use_result` and `timestamp` fields must round-trip without loss —
7092    /// proxies using this crate to relay messages to a viewer rely on those
7093    /// fields being preserved (the viewer reads `tool_use_result.answers`).
7094    #[test]
7095    fn test_user_message_preserves_tool_use_result_and_timestamp() {
7096        let json = r#"{
7097            "type":"user",
7098            "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"}]},
7099            "parent_tool_use_id":null,
7100            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
7101            "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
7102            "timestamp":"2026-05-12T23:12:04.121Z",
7103            "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"}}
7104        }"#;
7105
7106        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7107        let user = match output {
7108            ClaudeOutput::User(u) => u,
7109            other => panic!("Expected User message, got {:?}", other.message_type()),
7110        };
7111
7112        assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
7113        let raw = user
7114            .tool_use_result
7115            .as_ref()
7116            .expect("tool_use_result must be captured");
7117        assert_eq!(raw["answers"]["Color"], "Blue");
7118        assert_eq!(raw["questions"][0]["header"], "Color");
7119
7120        // Round-trip: re-serialize and confirm tool_use_result + timestamp
7121        // survive — the bug we're guarding against is that the proxy silently
7122        // drops these fields when relaying user messages.
7123        let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
7124        assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
7125        assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
7126        assert_eq!(
7127            reser["tool_use_result"]["questions"][0]["question"],
7128            "Which color do you prefer?"
7129        );
7130
7131        // Typed accessor: AskUserQuestionInput has the same shape as the
7132        // AskUserQuestion tool_use_result.
7133        let typed: crate::AskUserQuestionInput = user
7134            .tool_use_result_as::<crate::AskUserQuestionInput>()
7135            .expect("tool_use_result present")
7136            .expect("AskUserQuestionInput parses");
7137        assert_eq!(typed.questions.len(), 1);
7138        assert_eq!(typed.questions[0].header, "Color");
7139        let answers = typed.answers.expect("answers populated");
7140        assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
7141    }
7142
7143    /// User messages without `tool_use_result` / `timestamp` must still
7144    /// deserialize fine and serialize back without spuriously emitting nulls.
7145    #[test]
7146    fn test_user_message_without_tool_use_result_omits_field() {
7147        let json = r#"{
7148            "type":"user",
7149            "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
7150            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
7151        }"#;
7152
7153        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7154        let user = match output {
7155            ClaudeOutput::User(u) => u,
7156            _ => panic!("Expected User message"),
7157        };
7158        assert!(user.tool_use_result.is_none());
7159        assert!(user.timestamp.is_none());
7160
7161        let reser = serde_json::to_value(&user).unwrap();
7162        assert!(reser.get("tool_use_result").is_none());
7163        assert!(reser.get("timestamp").is_none());
7164    }
7165
7166    /// A `Task` tool result must expose subagent token / timing / tool-use
7167    /// accounting through the typed [`UserMessage::subagent_result`] accessor,
7168    /// including the nested per-model `usage` breakdown and `toolStats`.
7169    #[test]
7170    fn test_subagent_result_exposes_token_accounting() {
7171        let json = r#"{
7172            "type":"user",
7173            "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
7174            "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
7175            "tool_use_result":{
7176                "status":"completed",
7177                "prompt":"Count the .rs files.",
7178                "agentId":"ac4f0276e9d4b6232",
7179                "agentType":"Explore",
7180                "content":[{"type":"text","text":"21"}],
7181                "resolvedModel":"claude-haiku-4-5-20251001",
7182                "totalDurationMs":6869,
7183                "totalTokens":7834,
7184                "totalToolUseCount":1,
7185                "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
7186                "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
7187            }
7188        }"#;
7189
7190        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7191        let user = match output {
7192            ClaudeOutput::User(u) => u,
7193            _ => panic!("Expected User message"),
7194        };
7195
7196        let result = user.subagent_result().expect("subagent result parses");
7197        assert_eq!(result.agent_type.as_deref(), Some("Explore"));
7198        assert_eq!(
7199            result.resolved_model.as_deref(),
7200            Some("claude-haiku-4-5-20251001")
7201        );
7202        assert_eq!(result.total_tokens, Some(7834));
7203        assert_eq!(result.total_duration_ms, Some(6869));
7204        assert_eq!(result.total_tool_use_count, Some(1));
7205
7206        let usage = result.usage.expect("nested usage present");
7207        assert_eq!(usage.input_tokens, 6);
7208        assert_eq!(usage.cache_read_input_tokens, 7699);
7209
7210        let stats = result.tool_stats.expect("toolStats present");
7211        assert_eq!(stats.bash_count, 1);
7212    }
7213
7214    /// `tool_use_result` shapes that aren't subagent runs (e.g. AskUserQuestion)
7215    /// parse leniently into the all-`Option` [`SubagentResult`] with empty
7216    /// accounting rather than failing, so callers can probe without panicking.
7217    #[test]
7218    fn test_subagent_result_absent_for_non_task_result() {
7219        let json = r#"{
7220            "type":"user",
7221            "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
7222            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
7223            "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
7224        }"#;
7225
7226        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7227        let user = match output {
7228            ClaudeOutput::User(u) => u,
7229            _ => panic!("Expected User message"),
7230        };
7231
7232        let result = user.subagent_result().expect("lenient parse");
7233        assert_eq!(result.total_tokens, None);
7234        assert_eq!(result.agent_type, None);
7235    }
7236
7237    #[test]
7238    fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
7239        use serde_json::Value;
7240
7241        let raw: Value = serde_json::from_str(
7242            r#"{
7243            "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
7244            "fast_mode_state":"off",
7245            "fast_mode_disabled_reason":"not_first_party",
7246            "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
7247        }"#,
7248        )
7249        .unwrap();
7250        crate::io::assert_fully_wrapped(&raw);
7251
7252        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
7253        let ClaudeOutput::System(sys) = output else {
7254            panic!("expected System");
7255        };
7256        let init = sys.as_init().expect("parses as init");
7257        assert_eq!(
7258            init.fast_mode_disabled_reason,
7259            Some(crate::FastModeDisabledReason::NotFirstParty)
7260        );
7261        let errs = init.mcp_server_errors.unwrap();
7262        assert_eq!(errs.len(), 1);
7263        assert_eq!(errs[0].name, "broken");
7264        assert_eq!(errs[0].error_type, "invalid_config");
7265    }
7266
7267    #[test]
7268    fn test_code_change_published_fully_wrapped() {
7269        use super::{KnownSystemEvent, SystemSubtype};
7270        use serde_json::Value;
7271
7272        let raw: Value = serde_json::from_str(
7273            r#"{
7274            "type":"system","subtype":"code_change_published",
7275            "provider":"github","url":"https://github.com/owner/repo/pull/42",
7276            "repo":"owner/repo","identifier":"42",
7277            "uuid":"u1","session_id":"s1"
7278        }"#,
7279        )
7280        .unwrap();
7281        crate::io::assert_fully_wrapped(&raw);
7282
7283        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
7284        let ClaudeOutput::System(sys) = output else {
7285            panic!("expected System");
7286        };
7287        assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
7288        let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
7289            panic!("expected CodeChangePublished event");
7290        };
7291        assert_eq!(msg.provider, "github");
7292        assert_eq!(msg.repo, "owner/repo");
7293        assert_eq!(msg.identifier, "42");
7294
7295        assert!(sys.is_code_change_published());
7296        assert!(!sys.is_vcs_state_changed());
7297        let direct = sys.as_code_change_published().expect("direct accessor");
7298        assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
7299        assert!(sys.as_vcs_state_changed().is_none());
7300    }
7301
7302    #[test]
7303    fn test_feedback_draft_queued_fully_wrapped() {
7304        use super::{KnownSystemEvent, SystemSubtype};
7305        use serde_json::Value;
7306
7307        let raw: Value = serde_json::from_str(
7308            r#"{
7309            "type":"system","subtype":"feedback_draft_queued",
7310            "draft_id":"draft-1","draft_type":"bug_report",
7311            "title":"Tool output was truncated",
7312            "details_preview":"The last command omitted its final lines",
7313            "uuid":"u1","session_id":"s1","future_field":"preserved"
7314        }"#,
7315        )
7316        .unwrap();
7317        crate::io::assert_fully_wrapped(&raw);
7318
7319        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
7320        let ClaudeOutput::System(sys) = output else {
7321            panic!("expected System");
7322        };
7323        assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
7324        assert!(sys.is_feedback_draft_queued());
7325        assert!(!sys.is_vcs_state_changed());
7326
7327        let direct = sys
7328            .as_feedback_draft_queued()
7329            .expect("direct typed accessor");
7330        assert_eq!(direct.draft_id, "draft-1");
7331        assert_eq!(direct.draft_type, "bug_report");
7332        assert_eq!(direct.extra["future_field"], "preserved");
7333
7334        let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
7335            panic!("expected FeedbackDraftQueued event");
7336        };
7337        assert_eq!(known.title, "Tool output was truncated");
7338        assert_eq!(
7339            sys.typed_value().expect("typed value")["future_field"],
7340            "preserved"
7341        );
7342    }
7343
7344    #[test]
7345    fn test_cloud_session_delta_fully_wrapped() {
7346        use super::{KnownSystemEvent, SystemSubtype};
7347        use serde_json::Value;
7348
7349        let raw: Value = serde_json::from_str(
7350            r#"{
7351            "type":"system","subtype":"cloud_session_delta",
7352            "seq":3,"changed":["serving","connection"],
7353            "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
7354                "serving":{"state":"on"},"connection":{"state":"live"}},
7355            "uuid":"u1","session_id":"s1","future_field":"preserved"
7356        }"#,
7357        )
7358        .unwrap();
7359        crate::io::assert_fully_wrapped(&raw);
7360
7361        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
7362        let ClaudeOutput::System(sys) = output else {
7363            panic!("expected System");
7364        };
7365        assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
7366        assert!(sys.is_cloud_session_delta());
7367        assert!(!sys.is_feedback_draft_queued());
7368
7369        let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
7370        assert_eq!(direct.seq, 3);
7371        assert_eq!(direct.changed, vec!["serving", "connection"]);
7372        assert_eq!(direct.cloud_session["id"], "session_abc");
7373        assert_eq!(direct.extra["future_field"], "preserved");
7374
7375        let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
7376            panic!("expected CloudSessionDelta event");
7377        };
7378        assert_eq!(known.session_id, "s1");
7379        assert_eq!(
7380            sys.typed_value().expect("typed value")["future_field"],
7381            "preserved"
7382        );
7383    }
7384
7385    #[test]
7386    fn test_vcs_state_changed_fully_wrapped() {
7387        use super::{KnownSystemEvent, VcsMutationKind};
7388        use serde_json::Value;
7389
7390        for kind in ["commit", "push", "merge", "rebase"] {
7391            let raw: Value = serde_json::from_str(&format!(
7392                r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
7393                kind
7394            ))
7395            .unwrap();
7396            crate::io::assert_fully_wrapped(&raw);
7397
7398            let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
7399            let ClaudeOutput::System(sys) = output else {
7400                panic!("expected System");
7401            };
7402            let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
7403                panic!("expected VcsStateChanged event");
7404            };
7405            assert_eq!(msg.kind.as_str(), kind);
7406            assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
7407        }
7408
7409        // Unknown kinds are valid per the wire contract.
7410        let raw: Value = serde_json::from_str(
7411            r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
7412        )
7413        .unwrap();
7414        crate::io::assert_fully_wrapped(&raw);
7415        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
7416        let ClaudeOutput::System(sys) = output else {
7417            panic!("expected System");
7418        };
7419        let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
7420            panic!("expected VcsStateChanged event");
7421        };
7422        assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
7423
7424        assert!(sys.is_vcs_state_changed());
7425        let direct = sys.as_vcs_state_changed().expect("direct accessor");
7426        assert_eq!(direct.cwd, "/repo");
7427        assert!(sys.as_code_change_published().is_none());
7428    }
7429
7430    #[test]
7431    fn test_assistant_aborted_and_resume_flags_roundtrip() {
7432        let json = r#"{
7433            "type":"assistant",
7434            "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
7435            "session_id":"s1",
7436            "aborted":true,
7437            "resumed_from_incomplete_thinking":true
7438        }"#;
7439        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7440        let ClaudeOutput::Assistant(msg) = &output else {
7441            panic!("expected Assistant");
7442        };
7443        assert_eq!(msg.aborted, Some(true));
7444        assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
7445        let reserialized = serde_json::to_string(&output).unwrap();
7446        assert!(reserialized.contains("\"aborted\":true"));
7447        assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
7448
7449        // Absent flags stay absent on the wire.
7450        let json = r#"{
7451            "type":"assistant",
7452            "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
7453            "session_id":"s2"
7454        }"#;
7455        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7456        let reserialized = serde_json::to_string(&output).unwrap();
7457        assert!(!reserialized.contains("aborted"));
7458        assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
7459    }
7460
7461    #[test]
7462    fn test_user_tool_result_meta_roundtrip() {
7463        let json = r#"{
7464            "type":"user",
7465            "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
7466            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
7467            "tool_result_meta":[
7468                {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
7469                {"id":"toolu_2","non_execution_kind":"permission-rule"}
7470            ]
7471        }"#;
7472        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
7473        let ClaudeOutput::User(user) = &output else {
7474            panic!("expected User");
7475        };
7476        let meta = user.tool_result_meta.as_ref().unwrap();
7477        assert_eq!(meta.len(), 2);
7478        assert_eq!(meta[0].non_execution_kind.as_deref(), Some("user-rejected"));
7479        assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
7480        assert_eq!(meta[1].user_feedback, None);
7481
7482        let reserialized = serde_json::to_string(&output).unwrap();
7483        assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
7484        assert!(!reserialized.contains("\"user_feedback\":null"));
7485    }
7486
7487    /// CLI 2.1.222 added `scope` to `system/model_refusal_fallback`:
7488    /// "session" (main-thread swap, also the meaning when absent on older
7489    /// CLIs) vs "local" (subagent/side-question fallback only).
7490    #[test]
7491    fn model_refusal_fallback_scope_roundtrips_and_defaults() {
7492        use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
7493        let with_scope = serde_json::json!({
7494            "trigger": "refusal",
7495            "direction": "retry",
7496            "scope": "local",
7497            "original_model": "claude-fable-5",
7498            "fallback_model": "claude-opus-5",
7499            "request_id": null,
7500            "content": "Refused; retried on fallback model.",
7501            "uuid": "u1",
7502            "session_id": "s1"
7503        });
7504        let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
7505        assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
7506        assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
7507
7508        // Older CLIs omit scope — absent, not null, and treated as session
7509        // by consumers per the wire docs.
7510        let mut without = with_scope.clone();
7511        without.as_object_mut().unwrap().remove("scope");
7512        let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
7513        assert_eq!(msg.scope, None);
7514        assert_eq!(serde_json::to_value(&msg).unwrap(), without);
7515
7516        // Open enum: unknown scopes pass through verbatim.
7517        assert_eq!(
7518            RefusalFallbackScope::from("workspace").as_str(),
7519            "workspace"
7520        );
7521    }
7522}