1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type")]
10pub enum Entry {
11 #[serde(rename = "user")]
13 User(UserEntry),
14
15 #[serde(rename = "assistant")]
16 Assistant(AssistantEntry),
17
18 #[serde(rename = "system")]
19 System(SystemEntry),
20
21 #[serde(rename = "attachment")]
22 Attachment(AttachmentEntry),
23
24 #[serde(rename = "progress")]
25 Progress(ProgressEntry),
26
27 #[serde(rename = "permission-mode")]
29 PermissionMode(PermissionModeEntry),
30
31 #[serde(rename = "last-prompt")]
32 LastPrompt(LastPromptEntry),
33
34 #[serde(rename = "ai-title")]
35 AiTitle(AiTitleEntry),
36
37 #[serde(rename = "custom-title")]
38 CustomTitle(CustomTitleEntry),
39
40 #[serde(rename = "agent-name")]
41 AgentName(AgentNameEntry),
42
43 #[serde(rename = "agent-color")]
44 AgentColor(AgentColorEntry),
45
46 #[serde(rename = "agent-setting")]
47 AgentSetting(AgentSettingEntry),
48
49 #[serde(rename = "tag")]
50 Tag(TagEntry),
51
52 #[serde(rename = "summary")]
53 Summary(SummaryEntry),
54
55 #[serde(rename = "task-summary")]
56 TaskSummary(TaskSummaryEntry),
57
58 #[serde(rename = "pr-link")]
59 PrLink(PrLinkEntry),
60
61 #[serde(rename = "mode")]
62 Mode(ModeEntry),
63
64 #[serde(rename = "worktree-state")]
65 WorktreeState(WorktreeStateEntry),
66
67 #[serde(rename = "content-replacement")]
68 ContentReplacement(ContentReplacementEntry),
69
70 #[serde(rename = "file-history-snapshot")]
71 FileHistorySnapshot(FileHistorySnapshotEntry),
72
73 #[serde(rename = "attribution-snapshot")]
74 AttributionSnapshot(AttributionSnapshotEntry),
75
76 #[serde(rename = "queue-operation")]
77 QueueOperation(QueueOperationEntry),
78
79 #[serde(rename = "marble-origami-commit")]
80 ContextCollapseCommit(ContextCollapseCommitEntry),
81
82 #[serde(rename = "marble-origami-snapshot")]
83 ContextCollapseSnapshot(ContextCollapseSnapshotEntry),
84
85 #[serde(rename = "speculation-accept")]
86 SpeculationAccept(SpeculationAcceptEntry),
87
88 #[serde(other)]
92 Unknown,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct Envelope {
106 pub uuid: String,
107
108 pub parent_uuid: Option<String>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
114 pub logical_parent_uuid: Option<String>,
115
116 pub is_sidechain: bool,
117 pub session_id: String,
118 pub timestamp: String,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub user_type: Option<String>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub entrypoint: Option<String>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub cwd: Option<String>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub version: Option<String>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub git_branch: Option<String>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub slug: Option<String>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub agent_id: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub team_name: Option<String>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub agent_name: Option<String>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub agent_color: Option<String>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub prompt_id: Option<String>,
155
156 #[serde(rename = "isMeta", skip_serializing_if = "Option::is_none")]
158 pub is_meta: Option<bool>,
159
160 #[serde(rename = "forkedFrom", skip_serializing_if = "Option::is_none")]
162 pub forked_from: Option<ForkedFrom>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct ForkedFrom {
168 pub message_uuid: String,
169 pub session_id: String,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct UserEntry {
178 #[serde(flatten)]
179 pub envelope: Envelope,
180 pub message: UserMessage,
181
182 #[serde(rename = "toolUseResult", skip_serializing_if = "Option::is_none")]
185 pub tool_use_result: Option<Value>,
186
187 #[serde(
189 rename = "sourceToolAssistantUUID",
190 skip_serializing_if = "Option::is_none"
191 )]
192 pub source_tool_assistant_uuid: Option<String>,
193
194 #[serde(rename = "sourceToolUseID", skip_serializing_if = "Option::is_none")]
196 pub source_tool_use_id: Option<String>,
197
198 #[serde(rename = "permissionMode", skip_serializing_if = "Option::is_none")]
199 pub permission_mode: Option<String>,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub origin: Option<Value>,
203
204 #[serde(rename = "isCompactSummary", skip_serializing_if = "Option::is_none")]
205 pub is_compact_summary: Option<bool>,
206
207 #[serde(
208 rename = "isVisibleInTranscriptOnly",
209 skip_serializing_if = "Option::is_none"
210 )]
211 pub is_visible_in_transcript_only: Option<bool>,
212
213 #[serde(rename = "imagePasteIds", skip_serializing_if = "Option::is_none")]
214 pub image_paste_ids: Option<Vec<u64>>,
215
216 #[serde(rename = "planContent", skip_serializing_if = "Option::is_none")]
217 pub plan_content: Option<String>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct UserMessage {
222 pub role: UserRole,
223 pub content: UserContent,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "lowercase")]
228pub enum UserRole {
229 User,
230 #[serde(other)]
231 Unknown,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236#[serde(untagged)]
237pub enum UserContent {
238 Text(String),
239 Blocks(Vec<UserContentBlock>),
240 Other(Value),
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(tag = "type", rename_all = "snake_case")]
246pub enum UserContentBlock {
247 Text {
248 text: String,
249 },
250
251 ToolResult {
252 tool_use_id: String,
253 content: Value,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 is_error: Option<bool>,
259 },
260
261 Image {
262 source: ImageSource,
263 },
264
265 Document {
266 source: DocumentSource,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 title: Option<String>,
269 },
270
271 #[serde(other)]
273 Unknown,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(tag = "type", rename_all = "snake_case")]
278pub enum ImageSource {
279 Base64 {
280 media_type: String,
281 data: String,
282 },
283 Url {
284 url: String,
285 },
286 #[serde(other)]
288 Unknown,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292#[serde(tag = "type", rename_all = "snake_case")]
293pub enum DocumentSource {
294 Base64 {
295 media_type: String,
296 data: String,
297 },
298 Text {
299 data: String,
300 },
301 Url {
302 url: String,
303 },
304 #[serde(other)]
306 Unknown,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct AssistantEntry {
316 #[serde(flatten)]
317 pub envelope: Envelope,
318 pub message: AssistantMessage,
319
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub request_id: Option<String>,
322
323 #[serde(rename = "isApiErrorMessage", skip_serializing_if = "Option::is_none")]
324 pub is_api_error_message: Option<bool>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub error: Option<String>,
328
329 #[serde(rename = "apiErrorStatus", skip_serializing_if = "Option::is_none")]
333 pub api_error_status: Option<u16>,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
339 pub attribution_agent: Option<String>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
345 pub attribution_plugin: Option<String>,
346
347 #[serde(skip_serializing_if = "Option::is_none")]
351 pub attribution_skill: Option<String>,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct AssistantMessage {
356 pub id: String,
357 #[serde(rename = "type")]
359 pub msg_type: String,
360 pub role: AssistantRole,
361 #[serde(default)]
362 pub model: Option<String>,
363
364 #[serde(
366 default,
367 skip_serializing_if = "Option::is_none",
368 with = "opt_nullable"
369 )]
370 pub container: Option<Option<Value>>,
371
372 pub content: Vec<AssistantContentBlock>,
373
374 pub stop_reason: Option<String>,
377
378 pub stop_sequence: Option<String>,
380
381 #[serde(
384 default,
385 skip_serializing_if = "Option::is_none",
386 with = "opt_nullable"
387 )]
388 pub stop_details: Option<Option<Value>>,
389
390 pub usage: AssistantUsage,
391
392 #[serde(
394 default,
395 skip_serializing_if = "Option::is_none",
396 with = "opt_nullable"
397 )]
398 pub context_management: Option<Option<Value>>,
399
400 #[serde(
405 default,
406 skip_serializing_if = "Option::is_none",
407 with = "opt_nullable"
408 )]
409 pub diagnostics: Option<Option<AssistantDiagnostics>>,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
413#[serde(rename_all = "lowercase")]
414pub enum AssistantRole {
415 Assistant,
416 #[serde(other)]
417 Unknown,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
421#[serde(tag = "type", rename_all = "snake_case")]
422pub enum AssistantContentBlock {
423 Text {
424 text: String,
425 },
426
427 Thinking {
431 thinking: String,
432 signature: String,
433 },
434
435 RedactedThinking {
436 data: String,
437 },
438
439 ToolUse {
440 id: String,
441 name: String,
442 input: Value,
443 #[serde(skip_serializing_if = "Option::is_none")]
445 caller: Option<ToolUseCaller>,
446 },
447
448 #[serde(other)]
450 Unknown,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct ToolUseCaller {
455 #[serde(rename = "type")]
456 pub caller_type: String,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct AssistantUsage {
462 pub input_tokens: u64,
463 pub output_tokens: u64,
464
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub cache_creation_input_tokens: Option<u64>,
467
468 #[serde(skip_serializing_if = "Option::is_none")]
469 pub cache_read_input_tokens: Option<u64>,
470
471 #[serde(skip_serializing_if = "Option::is_none")]
472 pub server_tool_use: Option<ServerToolUse>,
473
474 #[serde(
476 default,
477 skip_serializing_if = "Option::is_none",
478 with = "opt_nullable"
479 )]
480 pub service_tier: Option<Option<Value>>,
481
482 #[serde(skip_serializing_if = "Option::is_none")]
483 pub cache_creation: Option<CacheCreation>,
484
485 #[serde(
487 default,
488 skip_serializing_if = "Option::is_none",
489 with = "opt_nullable"
490 )]
491 pub inference_geo: Option<Option<Value>>,
492
493 #[serde(
495 default,
496 skip_serializing_if = "Option::is_none",
497 with = "opt_nullable"
498 )]
499 pub iterations: Option<Option<Value>>,
500
501 #[serde(
503 default,
504 skip_serializing_if = "Option::is_none",
505 with = "opt_nullable"
506 )]
507 pub speed: Option<Option<Value>>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct ServerToolUse {
512 #[serde(default)]
513 pub web_search_requests: u64,
514 #[serde(default)]
515 pub web_fetch_requests: u64,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct CacheCreation {
520 #[serde(skip_serializing_if = "Option::is_none")]
521 pub ephemeral_1h_input_tokens: Option<u64>,
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub ephemeral_5m_input_tokens: Option<u64>,
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct UsageIteration {
528 pub input_tokens: u64,
529 pub output_tokens: u64,
530
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub cache_read_input_tokens: Option<u64>,
533
534 #[serde(skip_serializing_if = "Option::is_none")]
535 pub cache_creation_input_tokens: Option<u64>,
536
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub cache_creation: Option<CacheCreation>,
539
540 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
542 pub iter_type: Option<String>,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
554#[serde(rename_all = "camelCase")]
555pub struct SystemEntry {
556 #[serde(flatten)]
557 pub envelope: Envelope,
558
559 pub subtype: SystemSubtype,
560
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub content: Option<String>,
564
565 #[serde(skip_serializing_if = "Option::is_none")]
567 pub level: Option<String>,
568
569 #[serde(rename = "isMeta", skip_serializing_if = "Option::is_none")]
571 pub is_meta: Option<bool>,
572
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub cause: Option<Value>,
576
577 #[serde(skip_serializing_if = "Option::is_none")]
578 pub error: Option<Value>,
579
580 #[serde(rename = "retryInMs", skip_serializing_if = "Option::is_none")]
581 pub retry_in_ms: Option<f64>,
582
583 #[serde(rename = "retryAttempt", skip_serializing_if = "Option::is_none")]
584 pub retry_attempt: Option<u32>,
585
586 #[serde(rename = "maxRetries", skip_serializing_if = "Option::is_none")]
587 pub max_retries: Option<u32>,
588
589 #[serde(rename = "hookCount", skip_serializing_if = "Option::is_none")]
591 pub hook_count: Option<u32>,
592
593 #[serde(rename = "hookInfos", skip_serializing_if = "Option::is_none")]
594 pub hook_infos: Option<Vec<HookInfo>>,
595
596 #[serde(rename = "hookErrors", skip_serializing_if = "Option::is_none")]
597 pub hook_errors: Option<Vec<Value>>,
598
599 #[serde(
600 rename = "preventedContinuation",
601 skip_serializing_if = "Option::is_none"
602 )]
603 pub prevented_continuation: Option<bool>,
604
605 #[serde(rename = "stopReason", skip_serializing_if = "Option::is_none")]
606 pub stop_reason: Option<String>,
607
608 #[serde(rename = "hasOutput", skip_serializing_if = "Option::is_none")]
609 pub has_output: Option<bool>,
610
611 #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
612 pub tool_use_id: Option<String>,
613
614 #[serde(rename = "durationMs", skip_serializing_if = "Option::is_none")]
616 pub duration_ms: Option<f64>,
617
618 #[serde(rename = "messageCount", skip_serializing_if = "Option::is_none")]
619 pub message_count: Option<u32>,
620
621 #[serde(skip_serializing_if = "Option::is_none")]
623 pub url: Option<String>,
624
625 #[serde(rename = "upgradeNudge", skip_serializing_if = "Option::is_none")]
626 pub upgrade_nudge: Option<String>,
627
628 #[serde(rename = "compactMetadata", skip_serializing_if = "Option::is_none")]
630 pub compact_metadata: Option<CompactMetadata>,
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize)]
634#[serde(rename_all = "snake_case")]
635pub enum SystemSubtype {
636 ApiError,
637 AwaySummary,
638 BridgeStatus,
639 CompactBoundary,
640 Informational,
641 LocalCommand,
642 ScheduledTaskFire,
643 StopHookSummary,
644 TurnDuration,
645 MicrocompactBoundary,
646 PermissionRetry,
647 AgentsKilled,
648 #[serde(other)]
649 Unknown,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize)]
653#[serde(rename_all = "camelCase")]
654pub struct HookInfo {
655 pub command: String,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub duration_ms: Option<u64>,
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
661#[serde(rename_all = "camelCase")]
662pub struct PreservedSegment {
663 pub head_uuid: String,
664 pub anchor_uuid: String,
665 pub tail_uuid: String,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize)]
669#[serde(rename_all = "camelCase")]
670pub struct CompactMetadata {
671 pub trigger: String,
672 #[serde(skip_serializing_if = "Option::is_none")]
673 pub pre_tokens: Option<u64>,
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub post_tokens: Option<u64>,
676 #[serde(skip_serializing_if = "Option::is_none")]
677 pub duration_ms: Option<u64>,
678 #[serde(skip_serializing_if = "Option::is_none")]
679 pub preserved_segment: Option<PreservedSegment>,
680 #[serde(
681 rename = "preCompactDiscoveredTools",
682 skip_serializing_if = "Option::is_none"
683 )]
684 pub pre_compact_discovered_tools: Option<Vec<String>>,
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct AttachmentEntry {
693 #[serde(flatten)]
694 pub envelope: Envelope,
695 pub attachment: AttachmentData,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
699#[serde(tag = "type", rename_all = "snake_case")]
700pub enum AttachmentData {
701 HookSuccess(HookResultAttachment),
703 HookNonBlockingError(HookResultAttachment),
704 HookBlockingError(HookResultAttachment),
705 HookCancelled(HookResultAttachment),
706
707 HookAdditionalContext {
708 content: Vec<String>,
709 #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
710 hook_name: Option<String>,
711 #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
712 tool_use_id: Option<String>,
713 #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
714 hook_event: Option<String>,
715 },
716
717 HookPermissionDecision {
718 decision: String,
719 #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
720 hook_name: Option<String>,
721 #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
722 tool_use_id: Option<String>,
723 #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
724 hook_event: Option<String>,
725 },
726
727 HookStoppedContinuation {
731 message: String,
732 #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
733 hook_name: Option<String>,
734 #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
735 tool_use_id: Option<String>,
736 #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
737 hook_event: Option<String>,
738 },
739
740 HookSystemMessage {
742 content: String,
743 #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
744 hook_name: Option<String>,
745 #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
746 tool_use_id: Option<String>,
747 #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
748 hook_event: Option<String>,
749 },
750
751 File {
753 filename: String,
754 content: FileAttachmentContent,
755 #[serde(rename = "displayPath", skip_serializing_if = "Option::is_none")]
756 display_path: Option<String>,
757 },
758
759 EditedTextFile {
760 filename: String,
761 snippet: String,
763 },
764
765 Directory {
766 path: String,
767 content: String,
768 #[serde(rename = "displayPath")]
769 display_path: String,
770 },
771
772 CompactFileReference {
773 filename: String,
774 #[serde(rename = "displayPath")]
775 display_path: String,
776 },
777
778 CommandPermissions {
780 #[serde(rename = "allowedTools")]
781 allowed_tools: Vec<String>,
782 },
783
784 PlanMode {
786 #[serde(rename = "reminderType")]
787 reminder_type: String,
788 #[serde(rename = "isSubAgent")]
789 is_sub_agent: bool,
790 #[serde(rename = "planFilePath", skip_serializing_if = "Option::is_none")]
791 plan_file_path: Option<String>,
792 #[serde(rename = "planExists")]
793 plan_exists: bool,
794 },
795
796 PlanModeExit {
797 #[serde(rename = "planFilePath", skip_serializing_if = "Option::is_none")]
798 plan_file_path: Option<String>,
799 #[serde(rename = "planExists")]
800 plan_exists: bool,
801 },
802
803 AutoMode {
805 #[serde(rename = "reminderType")]
806 reminder_type: String,
807 },
808
809 AutoModeExit,
810
811 PlanFileReference {
815 #[serde(rename = "planFilePath")]
816 plan_file_path: String,
817 #[serde(rename = "planContent")]
818 plan_content: String,
819 },
820
821 SkillListing {
823 content: String,
824 #[serde(rename = "isInitial", skip_serializing_if = "Option::is_none")]
826 is_initial: Option<bool>,
827 #[serde(rename = "skillCount", skip_serializing_if = "Option::is_none")]
829 skill_count: Option<u32>,
830 },
831
832 DynamicSkill {
833 #[serde(rename = "skillDir")]
834 skill_dir: String,
835 #[serde(rename = "skillNames")]
836 skill_names: Vec<String>,
837 #[serde(rename = "displayPath")]
838 display_path: String,
839 },
840
841 InvokedSkills {
842 skills: Vec<InvokedSkill>,
843 },
844
845 TaskReminder {
847 content: Vec<Value>,
848 #[serde(rename = "itemCount")]
849 item_count: u32,
850 },
851
852 TodoReminder {
855 content: Vec<Value>,
856 #[serde(rename = "itemCount")]
857 item_count: u32,
858 },
859
860 Diagnostics {
862 files: Vec<DiagnosticsFile>,
863 #[serde(rename = "isNew")]
864 is_new: bool,
865 },
866
867 DateChange {
869 #[serde(rename = "newDate")]
870 new_date: String,
871 },
872
873 DeferredToolsDelta {
875 #[serde(rename = "addedNames")]
876 added_names: Vec<String>,
877 #[serde(rename = "addedLines", skip_serializing_if = "Option::is_none")]
880 added_lines: Option<Vec<String>>,
881 #[serde(rename = "removedNames", skip_serializing_if = "Option::is_none")]
882 removed_names: Option<Vec<String>>,
883 #[serde(rename = "readdedNames", skip_serializing_if = "Option::is_none")]
886 readded_names: Option<Vec<String>>,
887 },
888
889 McpInstructionsDelta {
890 #[serde(rename = "addedNames")]
891 added_names: Vec<String>,
892 #[serde(rename = "addedBlocks")]
893 added_blocks: Vec<String>,
894 #[serde(rename = "removedNames", skip_serializing_if = "Option::is_none")]
895 removed_names: Option<Vec<String>>,
896 },
897
898 AgentListingDelta {
903 #[serde(rename = "addedTypes")]
904 added_types: Vec<String>,
905 #[serde(rename = "addedLines")]
906 added_lines: Vec<String>,
907 #[serde(rename = "removedTypes")]
908 removed_types: Vec<String>,
909 #[serde(rename = "isInitial", skip_serializing_if = "Option::is_none")]
910 is_initial: Option<bool>,
911 #[serde(
912 rename = "showConcurrencyNote",
913 skip_serializing_if = "Option::is_none"
914 )]
915 show_concurrency_note: Option<bool>,
916 },
917
918 UltrathinkEffort {
920 level: String,
921 },
922
923 QueuedCommand {
925 prompt: Value,
929 #[serde(rename = "commandMode", skip_serializing_if = "Option::is_none")]
930 command_mode: Option<String>,
931 #[serde(rename = "imagePasteIds", skip_serializing_if = "Option::is_none")]
934 image_paste_ids: Option<Vec<u64>>,
935 },
936
937 NestedMemory {
939 path: String,
940 content: NestedMemoryContent,
941 #[serde(rename = "displayPath")]
942 display_path: String,
943 },
944
945 #[serde(other)]
947 Unknown,
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize)]
951pub struct NestedMemoryContent {
952 pub path: String,
953 #[serde(rename = "type")]
955 pub memory_type: String,
956 pub content: String,
957 #[serde(
958 rename = "contentDiffersFromDisk",
959 skip_serializing_if = "Option::is_none"
960 )]
961 pub content_differs_from_disk: Option<bool>,
962}
963
964#[derive(Debug, Clone, Serialize, Deserialize)]
965#[serde(rename_all = "camelCase")]
966pub struct HookResultAttachment {
967 #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
968 pub hook_name: Option<String>,
969 #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
970 pub tool_use_id: Option<String>,
971 #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
972 pub hook_event: Option<String>,
973 #[serde(skip_serializing_if = "Option::is_none")]
974 pub content: Option<String>,
975 #[serde(skip_serializing_if = "Option::is_none")]
976 pub stdout: Option<String>,
977 #[serde(skip_serializing_if = "Option::is_none")]
978 pub stderr: Option<String>,
979 #[serde(skip_serializing_if = "Option::is_none")]
980 pub exit_code: Option<i32>,
981 #[serde(skip_serializing_if = "Option::is_none")]
982 pub command: Option<String>,
983 #[serde(skip_serializing_if = "Option::is_none")]
984 pub duration_ms: Option<u64>,
985 #[serde(rename = "blockingError", skip_serializing_if = "Option::is_none")]
986 pub blocking_error: Option<Value>,
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
991#[serde(rename_all = "camelCase")]
992pub struct FileAttachmentContent {
993 #[serde(rename = "type")]
994 pub content_type: String,
995 pub file: FileData,
996}
997
998#[derive(Debug, Clone, Serialize, Deserialize)]
999#[serde(rename_all = "camelCase")]
1000pub struct FileData {
1001 pub file_path: String,
1002 #[serde(skip_serializing_if = "Option::is_none")]
1003 pub content: Option<String>,
1004 #[serde(rename = "numLines", skip_serializing_if = "Option::is_none")]
1005 pub num_lines: Option<u64>,
1006 #[serde(rename = "startLine", skip_serializing_if = "Option::is_none")]
1007 pub start_line: Option<u64>,
1008 #[serde(rename = "totalLines", skip_serializing_if = "Option::is_none")]
1009 pub total_lines: Option<u64>,
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct InvokedSkill {
1014 pub name: String,
1015 pub path: String,
1016 pub content: String,
1017}
1018
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1020pub struct DiagnosticsFile {
1021 pub uri: String,
1022 pub diagnostics: Vec<Diagnostic>,
1023}
1024
1025#[derive(Debug, Clone, Serialize, Deserialize)]
1033pub struct CacheMissReason {
1034 #[serde(rename = "type")]
1035 pub kind: String,
1036
1037 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub cache_missed_input_tokens: Option<u64>,
1039}
1040
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1045pub struct AssistantDiagnostics {
1046 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub cache_miss_reason: Option<CacheMissReason>,
1048}
1049
1050#[derive(Debug, Clone, Serialize, Deserialize)]
1051pub struct Diagnostic {
1052 pub message: String,
1053 pub severity: String,
1054 pub range: DiagnosticRange,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub source: Option<String>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1058 pub code: Option<Value>,
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize)]
1062pub struct DiagnosticRange {
1063 pub start: DiagnosticPosition,
1064 pub end: DiagnosticPosition,
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1068pub struct DiagnosticPosition {
1069 pub line: u32,
1070 pub character: u32,
1071}
1072
1073#[derive(Debug, Clone, Serialize, Deserialize)]
1078#[serde(rename_all = "camelCase")]
1079pub struct ProgressEntry {
1080 #[serde(flatten)]
1081 pub envelope: Envelope,
1082
1083 pub data: ProgressData,
1084
1085 #[serde(rename = "parentToolUseID", skip_serializing_if = "Option::is_none")]
1086 pub parent_tool_use_id: Option<String>,
1087
1088 #[serde(rename = "toolUseID", skip_serializing_if = "Option::is_none")]
1089 pub tool_use_id: Option<String>,
1090}
1091
1092#[derive(Debug, Clone, Serialize, Deserialize)]
1093#[serde(rename_all = "camelCase")]
1094pub struct ProgressData {
1095 #[serde(rename = "type")]
1096 pub data_type: String,
1097 #[serde(rename = "hookEvent", skip_serializing_if = "Option::is_none")]
1098 pub hook_event: Option<String>,
1099 #[serde(rename = "hookName", skip_serializing_if = "Option::is_none")]
1100 pub hook_name: Option<String>,
1101 #[serde(skip_serializing_if = "Option::is_none")]
1102 pub command: Option<String>,
1103 #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
1105 pub agent_id: Option<String>,
1106 #[serde(skip_serializing_if = "Option::is_none")]
1107 pub prompt: Option<String>,
1108 #[serde(skip_serializing_if = "Option::is_none")]
1109 pub message: Option<Value>,
1110 #[serde(skip_serializing_if = "Option::is_none")]
1112 pub query: Option<String>,
1113 #[serde(rename = "resultCount", skip_serializing_if = "Option::is_none")]
1114 pub result_count: Option<u32>,
1115 #[serde(rename = "elapsedTimeSeconds", skip_serializing_if = "Option::is_none")]
1117 pub elapsed_time_seconds: Option<f64>,
1118 #[serde(rename = "fullOutput", skip_serializing_if = "Option::is_none")]
1119 pub full_output: Option<String>,
1120 #[serde(rename = "output", skip_serializing_if = "Option::is_none")]
1121 pub output: Option<String>,
1122 #[serde(rename = "timeoutMs", skip_serializing_if = "Option::is_none")]
1123 pub timeout_ms: Option<u64>,
1124 #[serde(rename = "totalLines", skip_serializing_if = "Option::is_none")]
1125 pub total_lines: Option<u64>,
1126 #[serde(rename = "totalBytes", skip_serializing_if = "Option::is_none")]
1127 pub total_bytes: Option<u64>,
1128 #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
1129 pub task_id: Option<String>,
1130 #[serde(rename = "serverName", skip_serializing_if = "Option::is_none")]
1132 pub server_name: Option<String>,
1133 #[serde(rename = "status", skip_serializing_if = "Option::is_none")]
1134 pub status: Option<String>,
1135 #[serde(rename = "toolName", skip_serializing_if = "Option::is_none")]
1136 pub tool_name: Option<String>,
1137 #[serde(rename = "elapsedTimeMs", skip_serializing_if = "Option::is_none")]
1138 pub elapsed_time_ms: Option<f64>,
1139 #[serde(rename = "taskDescription", skip_serializing_if = "Option::is_none")]
1141 pub task_description: Option<String>,
1142 #[serde(rename = "taskType", skip_serializing_if = "Option::is_none")]
1143 pub task_type: Option<String>,
1144}
1145
1146#[derive(Debug, Clone, Serialize, Deserialize)]
1151#[serde(rename_all = "camelCase")]
1152pub struct PermissionModeEntry {
1153 pub permission_mode: String,
1154 pub session_id: String,
1155}
1156
1157#[derive(Debug, Clone, Serialize, Deserialize)]
1158#[serde(rename_all = "camelCase")]
1159pub struct LastPromptEntry {
1160 #[serde(skip_serializing_if = "Option::is_none")]
1161 pub last_prompt: Option<String>,
1162 #[serde(skip_serializing_if = "Option::is_none")]
1163 pub leaf_uuid: Option<String>,
1164 pub session_id: String,
1165}
1166
1167#[derive(Debug, Clone, Serialize, Deserialize)]
1168#[serde(rename_all = "camelCase")]
1169pub struct AiTitleEntry {
1170 pub ai_title: String,
1171 pub session_id: String,
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1175#[serde(rename_all = "camelCase")]
1176pub struct CustomTitleEntry {
1177 pub custom_title: String,
1178 pub session_id: String,
1179}
1180
1181#[derive(Debug, Clone, Serialize, Deserialize)]
1182#[serde(rename_all = "camelCase")]
1183pub struct AgentNameEntry {
1184 pub agent_name: String,
1185 pub session_id: String,
1186}
1187
1188#[derive(Debug, Clone, Serialize, Deserialize)]
1189#[serde(rename_all = "camelCase")]
1190pub struct AgentColorEntry {
1191 pub agent_color: String,
1192 pub session_id: String,
1193}
1194
1195#[derive(Debug, Clone, Serialize, Deserialize)]
1196#[serde(rename_all = "camelCase")]
1197pub struct AgentSettingEntry {
1198 pub agent_setting: String,
1199 pub session_id: String,
1200}
1201
1202#[derive(Debug, Clone, Serialize, Deserialize)]
1203#[serde(rename_all = "camelCase")]
1204pub struct TagEntry {
1205 pub tag: String,
1206 pub session_id: String,
1207}
1208
1209#[derive(Debug, Clone, Serialize, Deserialize)]
1210#[serde(rename_all = "camelCase")]
1211pub struct SummaryEntry {
1212 pub leaf_uuid: String,
1213 pub summary: String,
1214 pub session_id: String,
1215}
1216
1217#[derive(Debug, Clone, Serialize, Deserialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct TaskSummaryEntry {
1220 pub summary: String,
1221 pub session_id: String,
1222 pub timestamp: String,
1223}
1224
1225#[derive(Debug, Clone, Serialize, Deserialize)]
1226#[serde(rename_all = "camelCase")]
1227pub struct PrLinkEntry {
1228 pub session_id: String,
1229 pub pr_number: u32,
1230 pub pr_url: String,
1231 pub pr_repository: String,
1232 pub timestamp: String,
1233}
1234
1235#[derive(Debug, Clone, Serialize, Deserialize)]
1236#[serde(rename_all = "camelCase")]
1237pub struct ModeEntry {
1238 pub mode: SessionMode,
1239 pub session_id: String,
1240}
1241
1242#[derive(Debug, Clone, Serialize, Deserialize)]
1243#[serde(rename_all = "lowercase")]
1244pub enum SessionMode {
1245 Coordinator,
1246 Normal,
1247 #[serde(other)]
1248 Unknown,
1249}
1250
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1253#[serde(rename_all = "camelCase")]
1254pub struct WorktreeStateEntry {
1255 pub session_id: String,
1256 pub worktree_session: Option<PersistedWorktreeSession>,
1258}
1259
1260#[derive(Debug, Clone, Serialize, Deserialize)]
1261#[serde(rename_all = "camelCase")]
1262pub struct PersistedWorktreeSession {
1263 pub original_cwd: String,
1264 pub worktree_path: String,
1265 pub worktree_name: String,
1266 pub session_id: String,
1267
1268 #[serde(skip_serializing_if = "Option::is_none")]
1269 pub worktree_branch: Option<String>,
1270
1271 #[serde(skip_serializing_if = "Option::is_none")]
1272 pub original_branch: Option<String>,
1273
1274 #[serde(skip_serializing_if = "Option::is_none")]
1275 pub original_head_commit: Option<String>,
1276
1277 #[serde(rename = "tmuxSessionName", skip_serializing_if = "Option::is_none")]
1278 pub tmux_session_name: Option<String>,
1279
1280 #[serde(skip_serializing_if = "Option::is_none")]
1281 pub hook_based: Option<bool>,
1282}
1283
1284#[derive(Debug, Clone, Serialize, Deserialize)]
1285#[serde(rename_all = "camelCase")]
1286pub struct ContentReplacementEntry {
1287 pub session_id: String,
1288 pub replacements: Vec<Value>,
1289 #[serde(skip_serializing_if = "Option::is_none")]
1290 pub agent_id: Option<String>,
1291}
1292
1293#[derive(Debug, Clone, Serialize, Deserialize)]
1294#[serde(rename_all = "camelCase")]
1295pub struct FileHistorySnapshotEntry {
1296 pub message_id: String,
1297 pub snapshot: FileHistorySnapshot,
1298 pub is_snapshot_update: bool,
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize)]
1302#[serde(rename_all = "camelCase")]
1303pub struct FileHistorySnapshot {
1304 pub message_id: String,
1305 pub tracked_file_backups: Value,
1306 pub timestamp: String,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1310#[serde(rename_all = "camelCase")]
1311pub struct AttributionSnapshotEntry {
1312 pub message_id: String,
1313 pub surface: String,
1314 pub file_states: Value,
1315
1316 #[serde(skip_serializing_if = "Option::is_none")]
1317 pub prompt_count: Option<u32>,
1318
1319 #[serde(skip_serializing_if = "Option::is_none")]
1320 pub prompt_count_at_last_commit: Option<u32>,
1321
1322 #[serde(skip_serializing_if = "Option::is_none")]
1323 pub permission_prompt_count: Option<u32>,
1324
1325 #[serde(skip_serializing_if = "Option::is_none")]
1326 pub permission_prompt_count_at_last_commit: Option<u32>,
1327
1328 #[serde(skip_serializing_if = "Option::is_none")]
1329 pub escape_count: Option<u32>,
1330
1331 #[serde(skip_serializing_if = "Option::is_none")]
1332 pub escape_count_at_last_commit: Option<u32>,
1333}
1334
1335#[derive(Debug, Clone, Serialize, Deserialize)]
1336#[serde(rename_all = "camelCase")]
1337pub struct QueueOperationEntry {
1338 pub operation: String,
1339 pub timestamp: String,
1340 pub session_id: String,
1341 #[serde(skip_serializing_if = "Option::is_none")]
1342 pub content: Option<String>,
1343}
1344
1345#[derive(Debug, Clone, Serialize, Deserialize)]
1350#[serde(rename_all = "camelCase")]
1351pub struct ContextCollapseCommitEntry {
1352 pub session_id: String,
1353 pub collapse_id: String,
1354 pub summary_uuid: String,
1355 pub summary_content: String,
1356 pub summary: String,
1357 pub first_archived_uuid: String,
1358 pub last_archived_uuid: String,
1359}
1360
1361#[derive(Debug, Clone, Serialize, Deserialize)]
1362#[serde(rename_all = "camelCase")]
1363pub struct ContextCollapseSnapshotEntry {
1364 pub session_id: String,
1365 pub staged: Vec<StagedSpan>,
1366 pub armed: bool,
1367 pub last_spawn_tokens: u64,
1368}
1369
1370#[derive(Debug, Clone, Serialize, Deserialize)]
1371#[serde(rename_all = "camelCase")]
1372pub struct StagedSpan {
1373 pub start_uuid: String,
1374 pub end_uuid: String,
1375 pub summary: String,
1376 pub risk: f64,
1377 pub staged_at: u64,
1378}
1379
1380#[derive(Debug, Clone, Serialize, Deserialize)]
1381#[serde(rename_all = "camelCase")]
1382pub struct SpeculationAcceptEntry {
1383 pub timestamp: String,
1384 pub time_saved_ms: u64,
1385}
1386
1387mod opt_nullable {
1400 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1401
1402 pub fn serialize<S, T>(val: &Option<Option<T>>, ser: S) -> Result<S::Ok, S::Error>
1403 where
1404 S: Serializer,
1405 T: Serialize,
1406 {
1407 match val {
1408 None => unreachable!("skip_serializing_if = \"Option::is_none\" should prevent this"),
1409 Some(inner) => inner.serialize(ser),
1410 }
1411 }
1412
1413 pub fn deserialize<'de, D, T>(de: D) -> Result<Option<Option<T>>, D::Error>
1414 where
1415 D: Deserializer<'de>,
1416 T: Deserialize<'de>,
1417 {
1418 Ok(Some(Option::<T>::deserialize(de)?))
1419 }
1420}
1421
1422#[cfg(test)]
1423mod tests {
1424 use super::*;
1425
1426 #[test]
1427 fn attachment_data_unknown_variant() {
1428 let json = r#"{"type":"future_attachment_shape","some_field":42}"#;
1429 let v: AttachmentData = serde_json::from_str(json).unwrap();
1430 assert!(matches!(v, AttachmentData::Unknown));
1431 }
1432
1433 #[test]
1434 fn attachment_data_nested_memory_variant() {
1435 let json = r#"{"type":"nested_memory","path":"/p/CLAUDE.md","content":{"path":"/p/CLAUDE.md","type":"Project","content":"hi","contentDiffersFromDisk":false},"displayPath":"CLAUDE.md"}"#;
1436 let v: AttachmentData = serde_json::from_str(json).unwrap();
1437 match v {
1438 AttachmentData::NestedMemory {
1439 path,
1440 content,
1441 display_path,
1442 } => {
1443 assert_eq!(path, "/p/CLAUDE.md");
1444 assert_eq!(content.memory_type, "Project");
1445 assert_eq!(content.content, "hi");
1446 assert_eq!(content.content_differs_from_disk, Some(false));
1447 assert_eq!(display_path, "CLAUDE.md");
1448 }
1449 other => panic!("expected NestedMemory, got {other:?}"),
1450 }
1451 }
1452
1453 #[test]
1454 fn assistant_content_block_unknown_variant() {
1455 let json = r#"{"type":"future_modality","data":"foo"}"#;
1456 let v: AssistantContentBlock = serde_json::from_str(json).unwrap();
1457 assert!(matches!(v, AssistantContentBlock::Unknown));
1458 }
1459
1460 #[test]
1461 fn user_content_block_unknown_variant() {
1462 let json = r#"{"type":"video","url":"https://example.com"}"#;
1463 let v: UserContentBlock = serde_json::from_str(json).unwrap();
1464 assert!(matches!(v, UserContentBlock::Unknown));
1465 }
1466
1467 #[test]
1468 fn image_source_unknown_variant() {
1469 let json = r#"{"type":"s3_bucket","key":"foo"}"#;
1470 let v: ImageSource = serde_json::from_str(json).unwrap();
1471 assert!(matches!(v, ImageSource::Unknown));
1472 }
1473
1474 #[test]
1475 fn document_source_unknown_variant() {
1476 let json = r#"{"type":"pdf","data":"base64data"}"#;
1477 let v: DocumentSource = serde_json::from_str(json).unwrap();
1478 assert!(matches!(v, DocumentSource::Unknown));
1479 }
1480
1481 #[test]
1483 fn attachment_data_known_variant_unaffected() {
1484 let json = r#"{"type":"date_change","newDate":"2024-01-01"}"#;
1485 let v: AttachmentData = serde_json::from_str(json).unwrap();
1486 assert!(matches!(v, AttachmentData::DateChange { .. }));
1487 }
1488
1489 #[test]
1490 fn assistant_content_block_known_variant_unaffected() {
1491 let json = r#"{"type":"text","text":"hello"}"#;
1492 let v: AssistantContentBlock = serde_json::from_str(json).unwrap();
1493 assert!(matches!(v, AssistantContentBlock::Text { .. }));
1494 }
1495
1496 #[test]
1501 fn user_content_unknown_shape_does_not_fail() {
1502 let json = r#"{"type":"future_format","data":42}"#;
1503 let v: UserContent = serde_json::from_str(json).unwrap();
1504 assert!(matches!(v, UserContent::Other(_)));
1505 }
1506
1507 #[test]
1510 fn server_tool_use_missing_field_uses_default() {
1511 let json = r#"{"web_search_requests":3}"#;
1512 let v: ServerToolUse = serde_json::from_str(json).unwrap();
1513 assert_eq!(v.web_search_requests, 3);
1514 assert_eq!(v.web_fetch_requests, 0);
1515 }
1516
1517 #[test]
1519 fn user_role_unknown_value_does_not_fail() {
1520 let json = r#""operator""#;
1521 let v: UserRole = serde_json::from_str(json).unwrap();
1522 assert!(matches!(v, UserRole::Unknown));
1523 }
1524
1525 #[test]
1527 fn assistant_role_unknown_value_does_not_fail() {
1528 let json = r#""system_agent""#;
1529 let v: AssistantRole = serde_json::from_str(json).unwrap();
1530 assert!(matches!(v, AssistantRole::Unknown));
1531 }
1532
1533 #[test]
1535 fn session_mode_unknown_value_does_not_fail() {
1536 let json = r#""background""#;
1537 let v: SessionMode = serde_json::from_str(json).unwrap();
1538 assert!(matches!(v, SessionMode::Unknown));
1539 }
1540
1541 #[test]
1544 fn assistant_message_missing_model_uses_default() {
1545 let json = r#"{
1546 "id": "msg_err1",
1547 "type": "message",
1548 "role": "assistant",
1549 "content": [],
1550 "stop_reason": "error",
1551 "stop_sequence": null,
1552 "usage": {"input_tokens": 0, "output_tokens": 0}
1553 }"#;
1554 let v: AssistantMessage = serde_json::from_str(json).unwrap();
1555 assert!(v.model.is_none());
1556 }
1557
1558 #[test]
1561 fn assistant_diagnostics_round_trip_with_tokens() {
1562 let json =
1563 r#"{"cache_miss_reason":{"type":"tools_changed","cache_missed_input_tokens":41735}}"#;
1564 let v: AssistantDiagnostics = serde_json::from_str(json).unwrap();
1565 let back = serde_json::to_string(&v).unwrap();
1566 assert_eq!(back, json);
1567 }
1568
1569 #[test]
1572 fn assistant_diagnostics_round_trip_without_tokens() {
1573 let json = r#"{"cache_miss_reason":{"type":"unavailable"}}"#;
1574 let v: AssistantDiagnostics = serde_json::from_str(json).unwrap();
1575 let back = serde_json::to_string(&v).unwrap();
1576 assert_eq!(back, json);
1577 }
1578
1579 #[test]
1583 fn assistant_diagnostics_unknown_kind_passes_through() {
1584 let json = r#"{"cache_miss_reason":{"type":"future_reason_not_yet_seen"}}"#;
1585 let v: AssistantDiagnostics = serde_json::from_str(json).unwrap();
1586 let cmr = v.cache_miss_reason.as_ref().expect("cache_miss_reason set");
1587 assert_eq!(cmr.kind, "future_reason_not_yet_seen");
1588 }
1589
1590 #[test]
1593 fn assistant_message_with_diagnostics_round_trip() {
1594 let json = r#"{"id":"msg_dx","type":"message","role":"assistant","model":"claude-opus-4-7","content":[],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1},"diagnostics":{"cache_miss_reason":{"type":"system_changed","cache_missed_input_tokens":33656}}}"#;
1595 let v: AssistantMessage = serde_json::from_str(json).unwrap();
1596 let back = serde_json::to_string(&v).unwrap();
1597 assert_eq!(back, json);
1598 }
1599
1600 #[test]
1603 fn assistant_message_with_null_diagnostics_round_trips_as_null() {
1604 let json = r#"{"id":"msg_dn","type":"message","role":"assistant","model":"claude-opus-4-7","content":[],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1},"diagnostics":null}"#;
1605 let v: AssistantMessage = serde_json::from_str(json).unwrap();
1606 let back = serde_json::to_string(&v).unwrap();
1607 assert_eq!(back, json);
1608 }
1609
1610 #[test]
1614 fn assistant_message_without_diagnostics_omits_field() {
1615 let json = r#"{"id":"msg_da","type":"message","role":"assistant","model":"claude-opus-4-7","content":[],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1}}"#;
1616 let v: AssistantMessage = serde_json::from_str(json).unwrap();
1617 assert!(
1618 v.diagnostics.is_none(),
1619 "outer Option should be None when key absent"
1620 );
1621 let back = serde_json::to_string(&v).unwrap();
1622 assert_eq!(back, json, "absent field must not re-emit as null");
1623 }
1624
1625 #[test]
1626 fn assistant_entry_round_trip_with_attribution_and_diagnostics() {
1627 let json = r#"{"uuid":"a1","parentUuid":null,"isSidechain":true,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"assistant","attributionAgent":"plugin1:agent1","attributionPlugin":"plugin1","message":{"id":"msg1","type":"message","role":"assistant","model":"claude-opus-4-7","content":[],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1},"diagnostics":{"cache_miss_reason":{"type":"messages_changed","cache_missed_input_tokens":204}}}}"#;
1628 let v: Entry = serde_json::from_str(json).unwrap();
1629 let back = serde_json::to_string(&v).unwrap();
1630
1631 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1632 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1633 assert_eq!(roundtripped, original);
1634 }
1635
1636 #[test]
1637 fn assistant_entry_round_trip_with_attribution_skill() {
1638 let json = r#"{"uuid":"a2","parentUuid":null,"isSidechain":true,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"assistant","attributionAgent":"plugin1:agent1","attributionPlugin":"plugin1","attributionSkill":"plugin1:skill1","message":{"id":"msg1","type":"message","role":"assistant","model":"claude-opus-4-7","content":[],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1}}}"#;
1641 let v: Entry = serde_json::from_str(json).unwrap();
1642 let back = serde_json::to_string(&v).unwrap();
1643
1644 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1645 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1646 assert_eq!(roundtripped, original);
1647 }
1648
1649 #[test]
1652 fn assistant_entry_round_trip_with_api_error_status() {
1653 let json = r#"{"uuid":"a4","parentUuid":null,"isSidechain":false,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"assistant","isApiErrorMessage":true,"error":"rate limit","apiErrorStatus":429,"message":{"id":"msg1","type":"message","role":"assistant","model":"claude-opus-4-7","content":[],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1}}}"#;
1654 let v: Entry = serde_json::from_str(json).unwrap();
1655 let back = serde_json::to_string(&v).unwrap();
1656
1657 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1658 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1659 assert_eq!(roundtripped, original);
1660 }
1661
1662 #[test]
1665 fn attachment_plan_file_reference_round_trips() {
1666 let json = r##"{"uuid":"a8","parentUuid":null,"isSidechain":false,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"attachment","attachment":{"type":"plan_file_reference","planFilePath":"/tmp/plan.md","planContent":"# Plan body"}}"##;
1667 let v: Entry = serde_json::from_str(json).unwrap();
1668 let back = serde_json::to_string(&v).unwrap();
1669 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1670 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1671 assert_eq!(roundtripped, original);
1672 }
1673
1674 #[test]
1677 fn attachment_deferred_tools_delta_with_readded_names_round_trips() {
1678 let json = r#"{"uuid":"a7","parentUuid":null,"isSidechain":false,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"attachment","attachment":{"type":"deferred_tools_delta","addedNames":["A"],"addedLines":["- A: foo"],"removedNames":["B"],"readdedNames":["C"]}}"#;
1679 let v: Entry = serde_json::from_str(json).unwrap();
1680 let back = serde_json::to_string(&v).unwrap();
1681 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1682 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1683 assert_eq!(roundtripped, original);
1684 }
1685
1686 #[test]
1689 fn attachment_auto_mode_round_trips() {
1690 let json = r#"{"uuid":"a5","parentUuid":null,"isSidechain":false,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"attachment","attachment":{"type":"auto_mode","reminderType":"full"}}"#;
1691 let v: Entry = serde_json::from_str(json).unwrap();
1692 let back = serde_json::to_string(&v).unwrap();
1693 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1694 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1695 assert_eq!(roundtripped, original);
1696 }
1697
1698 #[test]
1699 fn attachment_auto_mode_exit_round_trips() {
1700 let json = r#"{"uuid":"a6","parentUuid":null,"isSidechain":false,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"attachment","attachment":{"type":"auto_mode_exit"}}"#;
1701 let v: Entry = serde_json::from_str(json).unwrap();
1702 let back = serde_json::to_string(&v).unwrap();
1703 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1704 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1705 assert_eq!(roundtripped, original);
1706 }
1707
1708 #[test]
1713 fn attachment_agent_listing_delta_round_trips() {
1714 let json = r#"{"uuid":"a3","parentUuid":null,"isSidechain":false,"sessionId":"s1","timestamp":"2026-05-05T00:00:00.000Z","type":"attachment","attachment":{"type":"agent_listing_delta","addedTypes":["Explore","plugin1:agent1"],"addedLines":["- Explore: Fast read-only search","- plugin1:agent1: example"],"removedTypes":[],"isInitial":true,"showConcurrencyNote":true}}"#;
1715 let v: Entry = serde_json::from_str(json).unwrap();
1716 let back = serde_json::to_string(&v).unwrap();
1717
1718 let original: serde_json::Value = serde_json::from_str(json).unwrap();
1719 let roundtripped: serde_json::Value = serde_json::from_str(&back).unwrap();
1720 assert_eq!(roundtripped, original);
1721 }
1722}
1723
1724#[cfg(test)]
1725mod last_prompt_tests {
1726 use super::*;
1727
1728 fn parse(line: &str) -> Entry {
1729 serde_json::from_str::<Entry>(line).expect("parse")
1730 }
1731
1732 #[test]
1733 fn last_prompt_old_format_inline_text() {
1734 let e = parse(r#"{"type":"last-prompt","lastPrompt":"hello world","sessionId":"S"}"#);
1735 match e {
1736 Entry::LastPrompt(x) => {
1737 assert_eq!(x.last_prompt.as_deref(), Some("hello world"));
1738 assert_eq!(x.leaf_uuid, None);
1739 assert_eq!(x.session_id, "S");
1740 }
1741 other => panic!("wrong variant: {other:?}"),
1742 }
1743 }
1744
1745 #[test]
1746 fn last_prompt_new_format_leaf_uuid_only() {
1747 let e = parse(r#"{"type":"last-prompt","leafUuid":"u1","sessionId":"S"}"#);
1748 match e {
1749 Entry::LastPrompt(x) => {
1750 assert_eq!(x.last_prompt, None);
1751 assert_eq!(x.leaf_uuid.as_deref(), Some("u1"));
1752 assert_eq!(x.session_id, "S");
1753 }
1754 other => panic!("wrong variant: {other:?}"),
1755 }
1756 }
1757
1758 #[test]
1759 fn last_prompt_hypothetical_both_fields() {
1760 let e = parse(
1761 r#"{"type":"last-prompt","lastPrompt":"inline","leafUuid":"u2","sessionId":"S"}"#,
1762 );
1763 match e {
1764 Entry::LastPrompt(x) => {
1765 assert_eq!(x.last_prompt.as_deref(), Some("inline"));
1766 assert_eq!(x.leaf_uuid.as_deref(), Some("u2"));
1767 assert_eq!(x.session_id, "S");
1768 }
1769 other => panic!("wrong variant: {other:?}"),
1770 }
1771 }
1772
1773 #[test]
1774 fn last_prompt_hypothetical_neither_field() {
1775 let e = parse(r#"{"type":"last-prompt","sessionId":"S"}"#);
1776 match e {
1777 Entry::LastPrompt(x) => {
1778 assert_eq!(x.last_prompt, None);
1779 assert_eq!(x.leaf_uuid, None);
1780 assert_eq!(x.session_id, "S");
1781 }
1782 other => panic!("wrong variant: {other:?}"),
1783 }
1784 }
1785}