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}