Skip to main content

claude_session_types/events/
mod.rs

1//! Complete event type hierarchy for Claude Code sessions
2//!
3//! This module provides a comprehensive type map for ALL event types found in
4//! Claude Code JSONL session files, based on detailed analysis in
5//! `research/TYPE_HIERARCHY.md`.
6//!
7//! # Design Philosophy
8//!
9//! These types serve as a **complete data map** - not for immediate parsing of
10//! everything, but to know where data lives and have typed access when needed.
11//!
12//! # Module Structure
13//!
14//! - `root` - Top-level event types (7 root types)
15//! - `message` - Message content types (5 content types)
16//! - `progress` - Progress event types (5+ progress types)
17//! - `attachment` - Attachment types (10+ types)
18//! - `tool_result` - Tool result types (5+ types)
19//! - `system` - System event subtypes (2+ types)
20//! - `metadata` - Common metadata structures
21//!
22//! # Type Hierarchy Overview
23//!
24//! ```text
25//! Level 1: Root Events (.type)
26//! ├── progress (82,200)      → progress::ProgressEvent
27//! ├── assistant (49,426)     → root::AssistantEvent
28//! ├── user (29,913)          → root::UserEvent
29//! ├── system (6)             → system::SystemEvent
30//! ├── file-history-snapshot  → root::FileHistorySnapshot
31//! ├── queue-operation        → root::QueueOperation
32//! └── summary                → root::SessionSummary
33//!
34//! Level 2: Message Content (.message.content[].type)
35//! ├── text (18,557)          → message::TextBlock
36//! ├── tool_use (30,782)      → message::ToolUseBlock
37//! ├── tool_result (29,848)   → message::ToolResultBlock
38//! ├── image (5)              → message::ImageBlock
39//! └── attachment             → attachment::AttachmentBlock
40//!
41//! Level 3: Progress Data (.data.type when root .type == 'progress')
42//! ├── bash_progress (49,591)  → progress::BashProgressData
43//! ├── hook_progress (31,668)  → progress::HookProgressData
44//! ├── agent_progress (1,371)  → progress::AgentProgressData
45//! ├── query_update            → progress::QueryUpdateData
46//! └── search_results_received → progress::SearchResultsData
47//!
48//! Level 4: Attachment Types (.attachment.type)
49//! ├── hook_success (31,648)            → attachment::HookSuccess
50//! ├── todo_reminder (2,914)            → attachment::TodoReminder
51//! ├── critical_system_reminder (1,096) → attachment::CriticalReminder
52//! ├── edited_text_file (302)           → attachment::EditedTextFile
53//! ├── edited_notebook_cell             → attachment::EditedNotebookCell
54//! ├── file_snapshot                    → attachment::FileSnapshot
55//! └── ... (more types)
56//!
57//! Level 5: Tool Result Types (.toolUseResult.type)
58//! ├── text (28,313)  → tool_result::TextResult
59//! ├── create (1,895) → tool_result::CreateResult
60//! ├── update (155)   → tool_result::UpdateResult
61//! ├── delete         → tool_result::DeleteResult
62//! ├── read           → tool_result::ReadResult
63//! └── error          → tool_result::ErrorResult
64//!
65//! Level 6: System Subtypes (.subtype when .type == 'system')
66//! ├── compact_boundary       → system::CompactBoundary
67//! ├── microcompact_boundary  → system::MicrocompactBoundary
68//! └── ... (more types)
69//! ```
70//!
71//! # Usage Examples
72//!
73//! ## Parsing Root Events
74//!
75//! ```rust
76//! use claude_session_types::events::{SessionEvent, ProgressData};
77//!
78//! let line = r#"{"type": "user", "uuid": "test-uuid", "sessionId": "session-1", "timestamp": "2024-01-01T00:00:00Z", "isSidechain": false, "message": {"role": "user", "content": "hello"}}"#;
79//! let event: SessionEvent = serde_json::from_str(line)?;
80//!
81//! match event {
82//!     SessionEvent::User(user) => {
83//!         // Access user message content
84//!         for _block in &user.message.content {
85//!             // Process content blocks
86//!         }
87//!     }
88//!     SessionEvent::Progress(progress) => {
89//!         // Access progress data
90//!         match &progress.data {
91//!             ProgressData::BashProgress(bash) => {
92//!                 println!("Output: {}", bash.full_output);
93//!             }
94//!             ProgressData::AgentProgress(agent) => {
95//!                 println!("Agent: {}", agent.agent_id);
96//!             }
97//!             _ => {}
98//!         }
99//!     }
100//!     _ => {}
101//! }
102//! # Ok::<(), serde_json::Error>(())
103//! ```
104//!
105//! ## Accessing Nested Content
106//!
107//! ```rust
108//! use claude_session_types::events::{SessionEvent, ContentBlock, ToolUseResult};
109//!
110//! # let event: SessionEvent = serde_json::from_str(r#"{"type": "user", "uuid": "test", "timestamp": "2024-01-01T00:00:00Z", "sessionId": "test", "parentUuid": null, "isSidechain": false, "cwd": "/test", "message": {"role": "user", "content": []}}"#)?;
111//! if let SessionEvent::User(user) = event {
112//!     for block in &user.message.content {
113//!         match block {
114//!             ContentBlock::Text(text) => {
115//!                 println!("Text: {}", text.text);
116//!             }
117//!             ContentBlock::ToolResult(result) => {
118//!                 println!("Tool result ID: {}", result.tool_use_id);
119//!
120//!                 // Access nested tool use result
121//!                 if let Some(tool_result) = &result.tool_use_result {
122//!                     match tool_result {
123//!                         ToolUseResult::Create(create) => {
124//!                             println!("Created: {}", create.file_path);
125//!                         }
126//!                         _ => {}
127//!                     }
128//!                 }
129//!             }
130//!             _ => {}
131//!         }
132//!     }
133//! }
134//! # Ok::<(), serde_json::Error>(())
135//! ```
136//!
137//! ## Working with Progress Events
138//!
139//! Progress events contain the full conversation context in
140//! `.data.normalized_messages[]`, which recursively contains all event types:
141//!
142//! ```rust
143//! use claude_session_types::events::SessionEvent;
144//! use claude_session_types::events::progress::NormalizedMessage;
145//!
146//! # let event: SessionEvent = serde_json::from_str(r#"{"type": "progress", "uuid": "test", "timestamp": "2024-01-01T00:00:00Z", "sessionId": "test", "parentUuid": null, "isSidechain": false, "cwd": "/test", "data": {"type": "bash_progress", "output": "", "fullOutput": "", "elapsedTimeSeconds": 0, "totalLines": 0, "message": {}, "normalizedMessages": []}}"#)?;
147//! if let SessionEvent::Progress(progress) = event {
148//!     // Access normalized messages (conversation replay)
149//!     if let Some(normalized) = progress.data.normalized_messages() {
150//!         for msg in normalized {
151//!             match msg {
152//!                 NormalizedMessage::User(_user) => {
153//!                     println!("User turn");
154//!                 }
155//!                 NormalizedMessage::Assistant(_assistant) => {
156//!                     println!("Assistant turn");
157//!                 }
158//!                 NormalizedMessage::Attachment(attachment) => {
159//!                     println!("Attachment type: {:?}", attachment.attachment_type);
160//!                 }
161//!                 _ => {}
162//!             }
163//!         }
164//!     }
165//! }
166//! # Ok::<(), serde_json::Error>(())
167//! ```
168
169pub mod attachment;
170pub mod message;
171pub mod metadata;
172pub mod progress;
173pub mod root;
174pub mod system;
175pub mod tool_result;
176
177// Re-export main types for convenience
178pub use attachment::{AttachmentBlock, AttachmentType};
179pub use message::{ContentBlock, MessageContent};
180pub use metadata::EventMetadata;
181pub use progress::{ProgressData, ProgressEvent};
182pub use root::{
183    AgentNameEvent, AiTitleEvent, AssistantMessage, AtisLatchEvent, BridgeSessionEvent,
184    CacheCreation, CustomTitleEvent, FileHistoryDeltaEvent, FileHistorySnapshot, LastPromptEvent,
185    ModeEvent, OriginInfo, PermissionModeEvent, QueueOperation, RootAttachmentEvent, SessionEvent,
186    SessionSummary, Snapshot, TokenUsage, UserTurnKind,
187};
188pub use system::{CompactMetadata, SystemEvent};
189pub use tool_result::ToolUseResult;
190
191// Re-export for backward compatibility with existing code
192pub use root::QueueOperation as QueueOperationEvent;
193pub use root::{AssistantEvent as AssistantMessageEvent, UserEvent as UserMessageEvent};
194
195// ============================================================================
196// Legacy Implementations for Backward Compatibility
197// ============================================================================
198
199impl SessionEvent {
200    /// Extract tags for indexing (legacy method)
201    ///
202    /// Tags enable fast filtering of events without parsing full content.
203    pub fn extract_tags(&self) -> Vec<String> {
204        match self {
205            Self::User(e) => {
206                let mut tags = vec!["user".to_string()];
207
208                if e.metadata.user_type.as_deref() == Some("external") {
209                    tags.push("prompt".to_string());
210                }
211
212                if e.tool_use_result.is_some() {
213                    tags.push("tool_result".to_string());
214                }
215
216                tags
217            }
218
219            Self::Assistant(e) => {
220                let mut tags = vec!["assistant".to_string()];
221                tags.push(e.message.model.clone());
222
223                for block in &e.message.content {
224                    match block {
225                        ContentBlock::Text(_) if !tags.contains(&"text".to_string()) => {
226                            tags.push("text".to_string());
227                        }
228                        ContentBlock::Text(_) => {}
229                        ContentBlock::ToolUse(tool) => {
230                            tags.push("tool_use".to_string());
231                            tags.push(tool.name.clone());
232                        }
233                        _ => {}
234                    }
235                }
236
237                tags
238            }
239
240            Self::Progress(e) => {
241                let mut tags = vec!["progress".to_string()];
242
243                match &e.data {
244                    ProgressData::BashProgress(_) => {
245                        tags.push("bash_progress".to_string());
246                    }
247                    ProgressData::AgentProgress(agent) => {
248                        tags.push("agent_progress".to_string());
249                        tags.push(agent.agent_id.clone());
250                        if let Some(slug) = &e.metadata.slug {
251                            tags.push(slug.clone());
252                        }
253                    }
254                    ProgressData::HookProgress(hook) => {
255                        tags.push("hook_progress".to_string());
256                        tags.push(hook.hook_name.clone());
257                    }
258                    ProgressData::QueryUpdate(_) => {
259                        tags.push("query_update".to_string());
260                    }
261                    ProgressData::SearchResultsReceived(_) => {
262                        tags.push("search_results".to_string());
263                    }
264                    ProgressData::WaitingForTask(task) => {
265                        tags.push("waiting_for_task".to_string());
266                        tags.push(task.task_type.clone());
267                    }
268                    ProgressData::Unknown => {
269                        tags.push("unknown_progress".to_string());
270                    }
271                }
272
273                tags
274            }
275
276            Self::System(e) => {
277                let mut tags = vec!["system".to_string()];
278
279                if let Some(subtype) = &e.subtype {
280                    tags.push(subtype.clone());
281
282                    if e.is_compact_boundary() {
283                        if let Some(meta) = &e.compact_metadata {
284                            tags.push(meta.trigger.clone());
285                        }
286                    }
287                }
288
289                tags
290            }
291
292            Self::FileSnapshot(_) => vec!["file_snapshot".to_string()],
293
294            Self::QueueOperation(e) => {
295                vec!["queue_operation".to_string(), e.operation.clone()]
296            }
297
298            Self::Summary(_) => vec!["summary".to_string()],
299
300            Self::Attachment(e) => {
301                vec!["attachment".to_string(), e.attachment.type_name().to_string()]
302            }
303            Self::CustomTitle(_) => vec!["custom_title".to_string()],
304            Self::AiTitle(_) => vec!["ai_title".to_string()],
305            Self::LastPrompt(_) => vec!["last_prompt".to_string()],
306            Self::BridgeSession(_) => vec!["bridge_session".to_string()],
307            Self::AtisLatch(_) => vec!["atis_latch".to_string()],
308            Self::Mode(e) => vec!["mode".to_string(), e.mode.clone()],
309            Self::PermissionMode(e) => {
310                vec!["permission_mode".to_string(), e.permission_mode.clone()]
311            }
312            Self::AgentName(_) => vec!["agent_name".to_string()],
313            Self::FileHistoryDelta(_) => vec!["file_history_delta".to_string()],
314
315            Self::Unknown => vec!["unknown".to_string()],
316        }
317    }
318
319    /// Check if this event is important for context extraction
320    pub fn is_context_relevant(&self) -> bool {
321        match self {
322            Self::Progress(e) => matches!(e.data, ProgressData::AgentProgress(_)),
323            Self::User(e) => e.metadata.user_type.as_deref() == Some("external"),
324            Self::Assistant(e) => e
325                .message
326                .content
327                .iter()
328                .any(|block| matches!(block, ContentBlock::Text(_))),
329            Self::System(e) => e.is_compact_boundary(),
330            _ => false,
331        }
332    }
333}
334
335impl root::TokenUsage {
336    /// Total tokens (input + output)
337    pub fn total(&self) -> u64 {
338        self.input_tokens + self.output_tokens
339    }
340
341    /// Total input including cache operations
342    pub fn total_input(&self) -> u64 {
343        self.input_tokens + self.cache_creation_input_tokens + self.cache_read_input_tokens
344    }
345
346    /// Effective input tokens with cache discount (90% for reads)
347    #[must_use]
348    #[allow(clippy::cast_precision_loss)]
349    pub fn effective_input(&self) -> f64 {
350        (self.input_tokens + self.cache_creation_input_tokens) as f64
351            + (self.cache_read_input_tokens as f64 * 0.1)
352    }
353
354    /// Calculate cost in USD based on model pricing
355    #[must_use]
356    #[allow(clippy::cast_precision_loss)]
357    pub fn calculate_cost(&self, model: &str) -> Option<f64> {
358        let (input_cost, output_cost) = match normalize_model_name(model) {
359            "haiku" => (1.0 / 1_000_000.0, 5.0 / 1_000_000.0),
360            "sonnet" => (3.0 / 1_000_000.0, 15.0 / 1_000_000.0),
361            "opus" => (15.0 / 1_000_000.0, 75.0 / 1_000_000.0),
362            _ => return None,
363        };
364
365        let cache_write_cost = input_cost * 1.25;
366        let cache_read_cost = input_cost * 0.1;
367
368        Some(
369            (self.input_tokens as f64 * input_cost)
370                + (self.output_tokens as f64 * output_cost)
371                + (self.cache_creation_input_tokens as f64 * cache_write_cost)
372                + (self.cache_read_input_tokens as f64 * cache_read_cost),
373        )
374    }
375
376    /// Format cost as human-readable string
377    #[must_use]
378    pub fn format_cost(&self, model: &str) -> String {
379        match self.calculate_cost(model) {
380            Some(cost) => {
381                if cost < 0.01 {
382                    format!("${cost:.4}")
383                } else {
384                    format!("${cost:.2}")
385                }
386            }
387            None => "Unknown model".to_string(),
388        }
389    }
390
391    /// Calculate approximate cost (deprecated)
392    #[deprecated(since = "0.1.0", note = "Use calculate_cost(model) instead")]
393    pub fn estimated_cost(&self) -> f64 {
394        self.calculate_cost("sonnet").unwrap_or(0.0)
395    }
396}
397
398/// Normalize model name to "haiku", "sonnet", or "opus"
399fn normalize_model_name(model: &str) -> &str {
400    let lower = model.to_lowercase();
401    if lower.contains("haiku") {
402        "haiku"
403    } else if lower.contains("sonnet") {
404        "sonnet"
405    } else if lower.contains("opus") {
406        "opus"
407    } else {
408        "unknown"
409    }
410}