claude_session_types/events/progress.rs
1//! Progress event types (Level 3)
2//!
3//! Progress events are the MOST FREQUENT event type in Claude Code sessions
4//! (~82k occurrences per large session). They contain real-time updates during
5//! tool execution and include the full conversation context in normalized messages.
6//!
7//! # Progress Data Types
8//!
9//! ```text
10//! Progress Data (.data.type when root .type == 'progress')
11//! ├── bash_progress (49,591) - Bash command execution
12//! ├── hook_progress (31,668) - Hook execution
13//! ├── agent_progress (1,371) - Agent spawning/execution
14//! ├── query_update (1,183) - Web search queries
15//! ├── search_results_received (1,181) - Web search results
16//! └── ... (more types)
17//! ```
18//!
19//! # Special Feature: Normalized Messages
20//!
21//! Progress events contain `.data.normalizedMessages[]` which is a COMPLETE
22//! CONVERSATION REPLAY. This explains why progress events dominate file size!
23//!
24//! Each progress event embeds:
25//! - The current operation (bash, agent, hook)
26//! - The triggering message (`.data.message`)
27//! - **Entire conversation history** (`.data.normalizedMessages[]`)
28//!
29//! This creates a recursive structure where progress events contain OTHER events!
30
31use chrono::{DateTime, Utc};
32use serde::{Deserialize, Serialize};
33use serde_json::Value as JsonValue;
34
35use super::metadata::EventMetadata;
36
37/// Progress event wrapper
38///
39/// Contains real-time progress updates during tool execution.
40///
41/// # Links
42///
43/// - `tool_use_id`: Links to `ToolUseBlock.id` that triggered this progress
44/// - `parent_tool_use_id`: For nested tool invocations (agents spawning agents)
45/// - `metadata.parent_uuid`: Links to parent message in conversation
46///
47/// # Frequency
48///
49/// ~82,200 occurrences per large session (MOST FREQUENT event type!)
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ProgressEvent {
52 /// Common event metadata
53 #[serde(flatten)]
54 pub metadata: EventMetadata,
55
56 /// Tool use ID that triggered this progress
57 ///
58 /// Links to `ToolUseBlock.id` in assistant message
59 #[serde(rename = "toolUseID")]
60 pub tool_use_id: Option<String>,
61
62 /// Parent tool use ID (for nested tools)
63 ///
64 /// Used when agents spawn sub-agents or tools invoke other tools
65 #[serde(rename = "parentToolUseID")]
66 pub parent_tool_use_id: Option<String>,
67
68 /// Progress data (bash, agent, hook, etc.)
69 pub data: ProgressData,
70}
71
72impl ProgressEvent {
73 /// Get UUID
74 pub fn uuid(&self) -> &str {
75 &self.metadata.uuid
76 }
77
78 /// Get parent UUID
79 pub fn parent_uuid(&self) -> Option<&str> {
80 self.metadata.parent_uuid.as_deref()
81 }
82
83 /// Get timestamp
84 pub fn timestamp(&self) -> DateTime<Utc> {
85 self.metadata.timestamp
86 }
87}
88
89/// Progress data discriminator
90///
91/// All possible progress data types.
92///
93/// # Special Note: Normalized Messages
94///
95/// Most progress data variants contain `.normalizedMessages[]` which is a
96/// COMPLETE conversation history replay. This makes progress events HUGE.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(tag = "type", rename_all = "snake_case")]
99pub enum ProgressData {
100 /// Bash command execution progress
101 ///
102 /// Real-time updates during bash command execution.
103 ///
104 /// # Example
105 ///
106 /// ```json
107 /// {
108 /// "type": "bash_progress",
109 /// "output": "Compiling...",
110 /// "fullOutput": "Building...\nCompiling...",
111 /// "elapsedTimeSeconds": 5,
112 /// "totalLines": 2,
113 /// "message": {...},
114 /// "normalizedMessages": [...]
115 /// }
116 /// ```
117 ///
118 /// # Frequency
119 ///
120 /// ~49,591 occurrences per large session (most common progress type)
121 BashProgress(BashProgressData),
122
123 /// Hook execution progress
124 ///
125 /// Updates during pre/post hook execution.
126 ///
127 /// # Example
128 ///
129 /// ```json
130 /// {
131 /// "type": "hook_progress",
132 /// "hookEvent": "pre-tool-use",
133 /// "hookName": "pre-commit",
134 /// "command": "./hooks/pre-commit.sh",
135 /// "message": {...},
136 /// "normalizedMessages": [...]
137 /// }
138 /// ```
139 ///
140 /// # Frequency
141 ///
142 /// ~31,668 occurrences per large session
143 HookProgress(HookProgressData),
144
145 /// Agent spawning and execution
146 ///
147 /// Updates when delegating to agents.
148 ///
149 /// # Example
150 ///
151 /// ```json
152 /// {
153 /// "type": "agent_progress",
154 /// "agentId": "abc123",
155 /// "prompt": "Implement feature X",
156 /// "message": {...},
157 /// "normalizedMessages": [...]
158 /// }
159 /// ```
160 ///
161 /// # Frequency
162 ///
163 /// ~1,371 occurrences per large session
164 AgentProgress(AgentProgressData),
165
166 /// Web search query update
167 ///
168 /// Shows query being sent to search engine.
169 ///
170 /// # Example
171 ///
172 /// ```json
173 /// {
174 /// "type": "query_update",
175 /// "query": "rust async tokio best practices"
176 /// }
177 /// ```
178 ///
179 /// # Frequency
180 ///
181 /// ~1,183 occurrences per large session (when web search enabled)
182 QueryUpdate(QueryUpdateData),
183
184 /// Web search results received
185 ///
186 /// Search results from web search tool.
187 ///
188 /// # Example
189 ///
190 /// ```json
191 /// {
192 /// "type": "search_results_received",
193 /// "results": [
194 /// {"title": "...", "url": "...", "snippet": "..."}
195 /// ]
196 /// }
197 /// ```
198 ///
199 /// # Frequency
200 ///
201 /// ~1,181 occurrences per large session
202 SearchResultsReceived(SearchResultsData),
203
204 /// Waiting for task completion
205 ///
206 /// Shows that execution is waiting for a task to complete.
207 ///
208 /// # Example
209 ///
210 /// ```json
211 /// {
212 /// "type": "waiting_for_task",
213 /// "taskDescription": "Write Bitstamp tests Phase 3",
214 /// "taskType": "local_agent"
215 /// }
216 /// ```
217 ///
218 /// # Frequency
219 ///
220 /// ~8 occurrences per large session (rare, used with background tasks)
221 WaitingForTask(WaitingForTaskData),
222
223 /// Unknown progress type (forward compatibility)
224 #[serde(other)]
225 Unknown,
226}
227
228impl ProgressData {
229 /// Extract agent ID if this is agent progress
230 pub fn agent_id(&self) -> Option<&str> {
231 match self {
232 Self::AgentProgress(data) => Some(&data.agent_id),
233 _ => None,
234 }
235 }
236
237 /// Extract agent prompt if this is agent progress
238 pub fn agent_prompt(&self) -> Option<&str> {
239 match self {
240 Self::AgentProgress(data) => Some(&data.prompt),
241 _ => None,
242 }
243 }
244
245 /// Get normalized messages if present
246 pub fn normalized_messages(&self) -> Option<&[NormalizedMessage]> {
247 match self {
248 Self::BashProgress(data) => Some(&data.normalized_messages),
249 Self::HookProgress(data) => Some(&data.normalized_messages),
250 Self::AgentProgress(data) => Some(&data.normalized_messages),
251 _ => None,
252 }
253 }
254
255 /// Get bash progress data if this is bash progress
256 pub fn as_bash_progress(&self) -> Option<&BashProgressData> {
257 match self {
258 Self::BashProgress(data) => Some(data),
259 _ => None,
260 }
261 }
262
263 /// Get agent progress data if this is agent progress
264 pub fn as_agent_progress(&self) -> Option<&AgentProgressData> {
265 match self {
266 Self::AgentProgress(data) => Some(data),
267 _ => None,
268 }
269 }
270
271 /// Get hook progress data if this is hook progress
272 pub fn as_hook_progress(&self) -> Option<&HookProgressData> {
273 match self {
274 Self::HookProgress(data) => Some(data),
275 _ => None,
276 }
277 }
278}
279
280/// Bash command execution progress
281///
282/// Most common progress type (~49k per session).
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct BashProgressData {
285 /// Incremental output (latest chunk)
286 pub output: String,
287
288 /// Full accumulated output (all chunks)
289 #[serde(rename = "fullOutput")]
290 pub full_output: String,
291
292 /// Elapsed time (seconds)
293 #[serde(rename = "elapsedTimeSeconds")]
294 pub elapsed_time_seconds: u64,
295
296 /// Total output lines
297 #[serde(rename = "totalLines")]
298 pub total_lines: u64,
299
300 /// Triggering message
301 pub message: JsonValue,
302
303 /// Complete conversation history (HUGE!)
304 #[serde(rename = "normalizedMessages")]
305 pub normalized_messages: Vec<NormalizedMessage>,
306}
307
308/// Hook execution progress
309///
310/// ~31,668 occurrences per large session.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct HookProgressData {
313 /// Hook event type: "pre-tool-use", "post-tool-use"
314 #[serde(rename = "hookEvent")]
315 pub hook_event: String,
316
317 /// Hook name (e.g., "pre-commit")
318 #[serde(rename = "hookName")]
319 pub hook_name: String,
320
321 /// Hook command being executed
322 pub command: String,
323
324 /// Triggering message
325 pub message: JsonValue,
326
327 /// Complete conversation history
328 #[serde(rename = "normalizedMessages")]
329 pub normalized_messages: Vec<NormalizedMessage>,
330}
331
332/// Agent spawning and execution
333///
334/// ~1,371 occurrences per large session.
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct AgentProgressData {
337 /// Agent ID (unique identifier)
338 #[serde(rename = "agentId")]
339 pub agent_id: String,
340
341 /// Agent task/prompt
342 pub prompt: String,
343
344 /// Triggering message
345 pub message: JsonValue,
346
347 /// Complete conversation history
348 #[serde(rename = "normalizedMessages")]
349 pub normalized_messages: Vec<NormalizedMessage>,
350}
351
352/// Web search query update
353///
354/// ~1,183 occurrences per large session.
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct QueryUpdateData {
357 /// Search query string
358 pub query: String,
359}
360
361/// Web search results received
362///
363/// ~1,181 occurrences per large session.
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct SearchResultsData {
366 /// Search results (unstructured JSON)
367 #[serde(flatten)]
368 pub results: JsonValue,
369}
370
371/// Waiting for task completion
372///
373/// ~8 occurrences per large session.
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct WaitingForTaskData {
376 /// Task description
377 #[serde(rename = "taskDescription")]
378 pub task_description: String,
379
380 /// Task type (e.g., "local_agent")
381 #[serde(rename = "taskType")]
382 pub task_type: String,
383}
384
385/// Normalized message in progress events
386///
387/// Progress events contain `.data.normalizedMessages[]` which is a COMPLETE
388/// conversation replay. This is a recursive structure that can contain ANY
389/// event type including more progress events!
390///
391/// # Why This Exists
392///
393/// Progress events need full conversation context to:
394/// - Resume interrupted operations
395/// - Handle retries with context
396/// - Display conversation state during long operations
397///
398/// # Structure
399///
400/// ```text
401/// NormalizedMessage (enum)
402/// ├── User - User turns
403/// ├── Assistant - Assistant turns
404/// ├── Progress - Nested progress events
405/// ├── Attachment - Attachments (hooks, todos, etc.)
406/// └── System - System messages
407/// ```
408#[derive(Debug, Clone, Serialize, Deserialize)]
409#[serde(tag = "type", rename_all = "lowercase")]
410pub enum NormalizedMessage {
411 /// User message in conversation replay
412 #[serde(rename = "user")]
413 User(NormalizedUserMessage),
414
415 /// Assistant message in conversation replay
416 #[serde(rename = "assistant")]
417 Assistant(NormalizedAssistantMessage),
418
419 /// Nested progress event (yes, progress events can contain progress events!)
420 #[serde(rename = "progress")]
421 Progress(JsonValue),
422
423 /// Attachment (hook, todo, etc.)
424 #[serde(rename = "attachment")]
425 Attachment(NormalizedAttachment),
426
427 /// System message
428 #[serde(rename = "system")]
429 System(JsonValue),
430
431 /// Unknown normalized message type
432 #[serde(other)]
433 Unknown,
434}
435
436/// User message in normalized messages
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct NormalizedUserMessage {
439 /// Message UUID
440 pub uuid: String,
441
442 /// Parent UUID
443 #[serde(rename = "parentUuid")]
444 pub parent_uuid: Option<String>,
445
446 /// Timestamp
447 pub timestamp: DateTime<Utc>,
448
449 /// Message content
450 pub message: super::message::MessageContent,
451
452 /// User type: "external" or "internal"
453 #[serde(rename = "userType")]
454 pub user_type: Option<String>,
455}
456
457/// Assistant message in normalized messages
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct NormalizedAssistantMessage {
460 /// Message UUID
461 pub uuid: String,
462
463 /// Parent UUID
464 #[serde(rename = "parentUuid")]
465 pub parent_uuid: Option<String>,
466
467 /// Timestamp
468 pub timestamp: DateTime<Utc>,
469
470 /// Message content
471 pub message: JsonValue,
472
473 /// Model name
474 pub model: Option<String>,
475}
476
477/// Attachment in normalized messages
478#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct NormalizedAttachment {
480 /// Attachment type and data
481 #[serde(flatten)]
482 pub attachment_type: super::attachment::AttachmentType,
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_parse_bash_progress() {
491 let json = r#"{
492 "type": "bash_progress",
493 "output": "Compiling...",
494 "fullOutput": "Building...\nCompiling...",
495 "elapsedTimeSeconds": 5,
496 "totalLines": 2,
497 "message": {},
498 "normalizedMessages": []
499 }"#;
500
501 let data: ProgressData = serde_json::from_str(json).unwrap();
502 assert!(matches!(data, ProgressData::BashProgress(_)));
503
504 if let ProgressData::BashProgress(bash) = data {
505 assert_eq!(bash.output, "Compiling...");
506 assert_eq!(bash.full_output, "Building...\nCompiling...");
507 assert_eq!(bash.elapsed_time_seconds, 5);
508 assert_eq!(bash.total_lines, 2);
509 }
510 }
511
512 #[test]
513 fn test_parse_agent_progress() {
514 let json = r#"{
515 "type": "agent_progress",
516 "agentId": "abc123",
517 "prompt": "Implement feature X",
518 "message": {},
519 "normalizedMessages": []
520 }"#;
521
522 let data: ProgressData = serde_json::from_str(json).unwrap();
523 assert!(matches!(data, ProgressData::AgentProgress(_)));
524
525 assert_eq!(data.agent_id(), Some("abc123"));
526 assert_eq!(data.agent_prompt(), Some("Implement feature X"));
527 }
528
529 #[test]
530 fn test_parse_hook_progress() {
531 let json = r#"{
532 "type": "hook_progress",
533 "hookEvent": "pre-tool-use",
534 "hookName": "pre-commit",
535 "command": "./hooks/pre-commit.sh",
536 "message": {},
537 "normalizedMessages": []
538 }"#;
539
540 let data: ProgressData = serde_json::from_str(json).unwrap();
541 assert!(matches!(data, ProgressData::HookProgress(_)));
542
543 if let ProgressData::HookProgress(hook) = data {
544 assert_eq!(hook.hook_event, "pre-tool-use");
545 assert_eq!(hook.hook_name, "pre-commit");
546 assert_eq!(hook.command, "./hooks/pre-commit.sh");
547 }
548 }
549
550 #[test]
551 fn test_parse_query_update() {
552 let json = r#"{
553 "type": "query_update",
554 "query": "rust async best practices"
555 }"#;
556
557 let data: ProgressData = serde_json::from_str(json).unwrap();
558 assert!(matches!(data, ProgressData::QueryUpdate(_)));
559
560 if let ProgressData::QueryUpdate(query) = data {
561 assert_eq!(query.query, "rust async best practices");
562 }
563 }
564
565 #[test]
566 fn test_parse_waiting_for_task() {
567 let json = r#"{
568 "type": "waiting_for_task",
569 "taskDescription": "Write Bitstamp tests Phase 3",
570 "taskType": "local_agent"
571 }"#;
572
573 let data: ProgressData = serde_json::from_str(json).unwrap();
574 assert!(matches!(data, ProgressData::WaitingForTask(_)));
575
576 if let ProgressData::WaitingForTask(task) = data {
577 assert_eq!(task.task_description, "Write Bitstamp tests Phase 3");
578 assert_eq!(task.task_type, "local_agent");
579 }
580 }
581
582 #[test]
583 fn test_normalized_messages_access() {
584 let json = r#"{
585 "type": "bash_progress",
586 "output": "test",
587 "fullOutput": "test",
588 "elapsedTimeSeconds": 1,
589 "totalLines": 1,
590 "message": {},
591 "normalizedMessages": []
592 }"#;
593
594 let data: ProgressData = serde_json::from_str(json).unwrap();
595 assert!(data.normalized_messages().is_some());
596 assert_eq!(data.normalized_messages().unwrap().len(), 0);
597 }
598}