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