Skip to main content

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}