Skip to main content

copilot_sdk/
events.rs

1// Copyright (c) 2026 Elias Bachaalany
2// SPDX-License-Identifier: MIT
3
4//! Session event types for the Copilot SDK.
5//!
6//! Events are received from the Copilot CLI during a session. They include
7//! assistant messages, tool executions, session lifecycle events, and more.
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12// =============================================================================
13// Nested Types (used within event data)
14// =============================================================================
15
16/// Handoff source type.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum HandoffSourceType {
20    Remote,
21    Local,
22}
23
24/// System message role.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum SystemMessageRole {
28    System,
29    Developer,
30}
31
32/// Repository info for handoff events.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct RepositoryInfo {
36    pub owner: String,
37    pub name: String,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub branch: Option<String>,
40}
41
42/// Attachment in user message.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct UserMessageAttachmentItem {
46    #[serde(rename = "type")]
47    pub attachment_type: super::AttachmentType,
48    pub path: String,
49    pub display_name: String,
50}
51
52/// Tool request in assistant message.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct ToolRequestItem {
56    pub tool_call_id: String,
57    pub name: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub arguments: Option<serde_json::Value>,
60}
61
62/// Tool execution result content.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ToolResultContent {
65    pub content: String,
66}
67
68/// Tool execution error.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ToolExecutionError {
71    pub message: String,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub code: Option<String>,
74}
75
76/// Hook error.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct HookError {
79    pub message: String,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub stack: Option<String>,
82}
83
84/// System message metadata.
85#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct SystemMessageMetadata {
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub prompt_version: Option<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub variables: Option<HashMap<String, serde_json::Value>>,
92}
93
94// =============================================================================
95// Event Data Types
96// =============================================================================
97
98/// Data for session.start event.
99#[derive(Debug, Clone, Default, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct SessionStartData {
102    #[serde(default)]
103    pub session_id: String,
104    #[serde(default)]
105    pub version: f64,
106    #[serde(default)]
107    pub producer: String,
108    #[serde(default)]
109    pub copilot_version: String,
110    #[serde(default)]
111    pub start_time: String,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub selected_model: Option<String>,
114}
115
116/// Data for session.resume event.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct SessionResumeData {
120    pub resume_time: String,
121    pub event_count: f64,
122}
123
124/// Data for session.error event.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct SessionErrorData {
128    pub error_type: String,
129    pub message: String,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub stack: Option<String>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub code: Option<f64>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub provider_call_id: Option<String>,
136}
137
138/// Data for session.idle event.
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140pub struct SessionIdleData {}
141
142/// Data for session.info event.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct SessionInfoData {
146    pub info_type: String,
147    pub message: String,
148}
149
150/// Data for session.model_change event.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct SessionModelChangeData {
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub previous_model: Option<String>,
156    pub new_model: String,
157}
158
159/// Data for session.handoff event.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct SessionHandoffData {
163    pub handoff_time: String,
164    pub source_type: HandoffSourceType,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub repository: Option<RepositoryInfo>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub context: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub summary: Option<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub remote_session_id: Option<String>,
173}
174
175/// Data for session.truncation event.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct SessionTruncationData {
179    pub token_limit: f64,
180    pub pre_truncation_tokens_in_messages: f64,
181    pub pre_truncation_messages_length: f64,
182    pub post_truncation_tokens_in_messages: f64,
183    pub post_truncation_messages_length: f64,
184    pub tokens_removed_during_truncation: f64,
185    pub messages_removed_during_truncation: f64,
186    pub performed_by: String,
187}
188
189/// Data for user.message event.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct UserMessageData {
193    pub content: String,
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub transformed_content: Option<String>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub attachments: Option<Vec<UserMessageAttachmentItem>>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub source: Option<String>,
200}
201
202/// Data for pending_messages.modified event.
203#[derive(Debug, Clone, Default, Serialize, Deserialize)]
204pub struct PendingMessagesModifiedData {}
205
206/// Data for assistant.turn_start event.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct AssistantTurnStartData {
210    pub turn_id: String,
211}
212
213/// Data for assistant.intent event.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct AssistantIntentData {
216    pub intent: String,
217}
218
219/// Data for assistant.reasoning event.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct AssistantReasoningData {
223    pub reasoning_id: String,
224    pub content: String,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub chunk_content: Option<String>,
227}
228
229/// Data for assistant.reasoning_delta event.
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct AssistantReasoningDeltaData {
233    pub reasoning_id: String,
234    pub delta_content: String,
235}
236
237/// Data for assistant.message event.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct AssistantMessageData {
241    pub message_id: String,
242    pub content: String,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub chunk_content: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub total_response_size_bytes: Option<f64>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub tool_requests: Option<Vec<ToolRequestItem>>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub parent_tool_call_id: Option<String>,
251}
252
253/// Data for assistant.message_delta event.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct AssistantMessageDeltaData {
257    pub message_id: String,
258    pub delta_content: String,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub total_response_size_bytes: Option<f64>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub parent_tool_call_id: Option<String>,
263}
264
265/// Data for assistant.turn_end event.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct AssistantTurnEndData {
269    pub turn_id: String,
270}
271
272/// Data for assistant.usage event.
273#[derive(Debug, Clone, Default, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct AssistantUsageData {
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub model: Option<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub input_tokens: Option<f64>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub output_tokens: Option<f64>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub cache_read_tokens: Option<f64>,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub cache_write_tokens: Option<f64>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub cost: Option<f64>,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub duration: Option<f64>,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub initiator: Option<String>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub api_call_id: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub provider_call_id: Option<String>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub quota_snapshots: Option<HashMap<String, serde_json::Value>>,
298}
299
300/// Data for abort event.
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct AbortData {
303    pub reason: String,
304}
305
306/// Data for tool.user_requested event.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct ToolUserRequestedData {
310    pub tool_call_id: String,
311    pub tool_name: String,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub arguments: Option<serde_json::Value>,
314}
315
316/// Data for tool.execution_start event.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct ToolExecutionStartData {
320    pub tool_call_id: String,
321    pub tool_name: String,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub arguments: Option<serde_json::Value>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub parent_tool_call_id: Option<String>,
326}
327
328/// Data for tool.execution_partial_result event.
329#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct ToolExecutionPartialResultData {
332    pub tool_call_id: String,
333    pub partial_output: String,
334}
335
336/// Data for tool.execution_complete event.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338#[serde(rename_all = "camelCase")]
339pub struct ToolExecutionCompleteData {
340    pub tool_call_id: String,
341    pub success: bool,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub is_user_requested: Option<bool>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub result: Option<ToolResultContent>,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub error: Option<ToolExecutionError>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub tool_telemetry: Option<HashMap<String, serde_json::Value>>,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub parent_tool_call_id: Option<String>,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub mcp_server_name: Option<String>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub mcp_tool_name: Option<String>,
356}
357
358/// Data for custom_agent.started event.
359#[derive(Debug, Clone, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase")]
361pub struct CustomAgentStartedData {
362    pub tool_call_id: String,
363    pub agent_name: String,
364    pub agent_display_name: String,
365    pub agent_description: String,
366}
367
368/// Data for custom_agent.completed event.
369#[derive(Debug, Clone, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct CustomAgentCompletedData {
372    pub tool_call_id: String,
373    pub agent_name: String,
374}
375
376/// Data for custom_agent.failed event.
377#[derive(Debug, Clone, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct CustomAgentFailedData {
380    pub tool_call_id: String,
381    pub agent_name: String,
382    pub error: String,
383}
384
385/// Data for custom_agent.selected event.
386#[derive(Debug, Clone, Serialize, Deserialize)]
387#[serde(rename_all = "camelCase")]
388pub struct CustomAgentSelectedData {
389    pub agent_name: String,
390    pub agent_display_name: String,
391    pub tools: Vec<String>,
392}
393
394/// Data for hook.start event.
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct HookStartData {
398    pub hook_invocation_id: String,
399    pub hook_type: String,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub input: Option<serde_json::Value>,
402}
403
404/// Data for hook.end event.
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[serde(rename_all = "camelCase")]
407pub struct HookEndData {
408    pub hook_invocation_id: String,
409    pub hook_type: String,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub output: Option<serde_json::Value>,
412    pub success: bool,
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub error: Option<HookError>,
415}
416
417/// Data for system.message event.
418#[derive(Debug, Clone, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase")]
420pub struct SystemMessageEventData {
421    pub content: String,
422    pub role: SystemMessageRole,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub name: Option<String>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub metadata: Option<SystemMessageMetadata>,
427}
428
429/// Data for session.compaction_start event.
430#[derive(Debug, Clone, Default, Serialize, Deserialize)]
431pub struct SessionCompactionStartData {}
432
433/// Tokens used during compaction.
434#[derive(Debug, Clone, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct CompactionTokensUsed {
437    #[serde(default)]
438    pub input: f64,
439    #[serde(default)]
440    pub output: f64,
441    #[serde(default)]
442    pub cached_input: f64,
443}
444
445/// Data for session.compaction_complete event.
446#[derive(Debug, Clone, Serialize, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub struct SessionCompactionCompleteData {
449    pub success: bool,
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub error: Option<String>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub pre_compaction_tokens: Option<f64>,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub post_compaction_tokens: Option<f64>,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub pre_compaction_messages_length: Option<f64>,
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub post_compaction_messages_length: Option<f64>,
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub compaction_tokens_used: Option<CompactionTokensUsed>,
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub messages_removed: Option<f64>,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub tokens_removed: Option<f64>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub summary_content: Option<String>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub checkpoint_number: Option<f64>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub checkpoint_path: Option<String>,
472}
473
474/// Shutdown type for session.shutdown event.
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(rename_all = "lowercase")]
477pub enum ShutdownType {
478    Routine,
479    Error,
480}
481
482/// Code changes reported in shutdown event.
483#[derive(Debug, Clone, Default, Serialize, Deserialize)]
484#[serde(rename_all = "camelCase")]
485pub struct ShutdownCodeChanges {
486    #[serde(default)]
487    pub lines_added: f64,
488    #[serde(default)]
489    pub lines_removed: f64,
490    #[serde(default)]
491    pub files_modified: Vec<String>,
492}
493
494/// Data for session.shutdown event.
495#[derive(Debug, Clone, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct SessionShutdownData {
498    pub shutdown_type: ShutdownType,
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub error_reason: Option<String>,
501    #[serde(default)]
502    pub total_premium_requests: f64,
503    #[serde(default)]
504    pub total_api_duration_ms: f64,
505    #[serde(default)]
506    pub session_start_time: f64,
507    #[serde(default)]
508    pub code_changes: ShutdownCodeChanges,
509    #[serde(default)]
510    pub model_metrics: HashMap<String, serde_json::Value>,
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub current_model: Option<String>,
513}
514
515/// Data for session.snapshot_rewind event.
516#[derive(Debug, Clone, Serialize, Deserialize)]
517#[serde(rename_all = "camelCase")]
518pub struct SessionSnapshotRewindData {
519    pub up_to_event_id: String,
520    #[serde(default)]
521    pub events_removed: f64,
522}
523
524/// Data for session.usage_info event.
525#[derive(Debug, Clone, Serialize, Deserialize)]
526#[serde(rename_all = "camelCase")]
527pub struct SessionUsageInfoData {
528    #[serde(default)]
529    pub token_limit: f64,
530    #[serde(default)]
531    pub current_tokens: f64,
532    #[serde(default)]
533    pub messages_length: f64,
534}
535
536/// Data for tool.execution_progress event.
537#[derive(Debug, Clone, Serialize, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct ToolExecutionProgressData {
540    pub tool_call_id: String,
541    pub progress_message: String,
542}
543
544/// Data for skill.invoked event.
545#[derive(Debug, Clone, Serialize, Deserialize)]
546#[serde(rename_all = "camelCase")]
547pub struct SkillInvokedData {
548    pub name: String,
549    pub path: String,
550    pub content: String,
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub allowed_tools: Option<Vec<String>>,
553}
554
555// =============================================================================
556// Session Event (Discriminated Union)
557// =============================================================================
558
559/// Event data variants - the payload of each event type.
560#[derive(Debug, Clone, Serialize, Deserialize)]
561#[serde(untagged)]
562pub enum SessionEventData {
563    SessionStart(SessionStartData),
564    SessionResume(SessionResumeData),
565    SessionError(SessionErrorData),
566    SessionIdle(SessionIdleData),
567    SessionInfo(SessionInfoData),
568    SessionModelChange(SessionModelChangeData),
569    SessionHandoff(SessionHandoffData),
570    SessionTruncation(SessionTruncationData),
571    UserMessage(UserMessageData),
572    PendingMessagesModified(PendingMessagesModifiedData),
573    AssistantTurnStart(AssistantTurnStartData),
574    AssistantIntent(AssistantIntentData),
575    AssistantReasoning(AssistantReasoningData),
576    AssistantReasoningDelta(AssistantReasoningDeltaData),
577    AssistantMessage(AssistantMessageData),
578    AssistantMessageDelta(AssistantMessageDeltaData),
579    AssistantTurnEnd(AssistantTurnEndData),
580    AssistantUsage(AssistantUsageData),
581    Abort(AbortData),
582    ToolUserRequested(ToolUserRequestedData),
583    ToolExecutionStart(ToolExecutionStartData),
584    ToolExecutionPartialResult(ToolExecutionPartialResultData),
585    ToolExecutionComplete(ToolExecutionCompleteData),
586    ToolExecutionProgress(ToolExecutionProgressData),
587    CustomAgentStarted(CustomAgentStartedData),
588    CustomAgentCompleted(CustomAgentCompletedData),
589    CustomAgentFailed(CustomAgentFailedData),
590    CustomAgentSelected(CustomAgentSelectedData),
591    HookStart(HookStartData),
592    HookEnd(HookEndData),
593    SystemMessage(SystemMessageEventData),
594    SessionCompactionStart(SessionCompactionStartData),
595    SessionCompactionComplete(SessionCompactionCompleteData),
596    SessionShutdown(SessionShutdownData),
597    SessionSnapshotRewind(SessionSnapshotRewindData),
598    SessionUsageInfo(SessionUsageInfoData),
599    SkillInvoked(SkillInvokedData),
600    /// Unknown event - preserves raw JSON for forward compatibility.
601    Unknown(serde_json::Value),
602}
603
604/// Raw session event as received from the CLI.
605///
606/// The event has common fields (id, timestamp, type) and a data payload
607/// that varies based on the event type.
608#[derive(Debug, Clone, Serialize, Deserialize)]
609#[serde(rename_all = "camelCase")]
610pub struct RawSessionEvent {
611    pub id: String,
612    pub timestamp: String,
613    #[serde(rename = "type")]
614    pub event_type: String,
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub parent_id: Option<String>,
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub ephemeral: Option<bool>,
619    pub data: serde_json::Value,
620}
621
622/// A parsed session event with typed data.
623#[derive(Debug, Clone)]
624pub struct SessionEvent {
625    /// Unique event ID.
626    pub id: String,
627    /// ISO 8601 timestamp.
628    pub timestamp: String,
629    /// Original type string (e.g., "assistant.message").
630    pub event_type: String,
631    /// Parent event ID, if any.
632    pub parent_id: Option<String>,
633    /// Whether this event is ephemeral.
634    pub ephemeral: Option<bool>,
635    /// Typed event data.
636    pub data: SessionEventData,
637}
638
639impl SessionEvent {
640    /// Parse a session event from JSON.
641    pub fn from_json(json: &serde_json::Value) -> Result<Self, serde_json::Error> {
642        let raw: RawSessionEvent = serde_json::from_value(json.clone())?;
643        Ok(Self::from_raw(raw))
644    }
645
646    /// Convert a raw event to a typed event.
647    pub fn from_raw(raw: RawSessionEvent) -> Self {
648        let data = parse_event_data(&raw.event_type, raw.data);
649        Self {
650            id: raw.id,
651            timestamp: raw.timestamp,
652            event_type: raw.event_type,
653            parent_id: raw.parent_id,
654            ephemeral: raw.ephemeral,
655            data,
656        }
657    }
658
659    // =========================================================================
660    // Type checking helpers
661    // =========================================================================
662
663    /// Check if this is an assistant message event.
664    pub fn is_assistant_message(&self) -> bool {
665        matches!(self.data, SessionEventData::AssistantMessage(_))
666    }
667
668    /// Check if this is an assistant message delta event.
669    pub fn is_assistant_message_delta(&self) -> bool {
670        matches!(self.data, SessionEventData::AssistantMessageDelta(_))
671    }
672
673    /// Check if this is a session idle event.
674    pub fn is_session_idle(&self) -> bool {
675        matches!(self.data, SessionEventData::SessionIdle(_))
676    }
677
678    /// Check if this is a session error event.
679    pub fn is_session_error(&self) -> bool {
680        matches!(self.data, SessionEventData::SessionError(_))
681    }
682
683    /// Check if this is a terminal event (session ended).
684    pub fn is_terminal(&self) -> bool {
685        matches!(
686            self.data,
687            SessionEventData::SessionIdle(_) | SessionEventData::SessionError(_)
688        )
689    }
690
691    // =========================================================================
692    // Data extraction helpers
693    // =========================================================================
694
695    /// Get assistant message data if this is an assistant.message event.
696    pub fn as_assistant_message(&self) -> Option<&AssistantMessageData> {
697        match &self.data {
698            SessionEventData::AssistantMessage(data) => Some(data),
699            _ => None,
700        }
701    }
702
703    /// Get assistant message delta data if this is an assistant.message_delta event.
704    pub fn as_assistant_message_delta(&self) -> Option<&AssistantMessageDeltaData> {
705        match &self.data {
706            SessionEventData::AssistantMessageDelta(data) => Some(data),
707            _ => None,
708        }
709    }
710
711    /// Get session error data if this is a session.error event.
712    pub fn as_session_error(&self) -> Option<&SessionErrorData> {
713        match &self.data {
714            SessionEventData::SessionError(data) => Some(data),
715            _ => None,
716        }
717    }
718
719    /// Get tool execution complete data if this is a tool.execution_complete event.
720    pub fn as_tool_execution_complete(&self) -> Option<&ToolExecutionCompleteData> {
721        match &self.data {
722            SessionEventData::ToolExecutionComplete(data) => Some(data),
723            _ => None,
724        }
725    }
726
727    /// Extract the content from an assistant message or delta.
728    pub fn content(&self) -> Option<&str> {
729        match &self.data {
730            SessionEventData::AssistantMessage(data) => Some(&data.content),
731            SessionEventData::AssistantMessageDelta(data) => Some(&data.delta_content),
732            _ => None,
733        }
734    }
735}
736
737/// Parse event data based on event type string.
738fn parse_event_data(event_type: &str, data: serde_json::Value) -> SessionEventData {
739    match event_type {
740        "session.start" => serde_json::from_value(data)
741            .map(SessionEventData::SessionStart)
742            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
743        "session.resume" => serde_json::from_value(data)
744            .map(SessionEventData::SessionResume)
745            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
746        "session.error" => serde_json::from_value(data)
747            .map(SessionEventData::SessionError)
748            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
749        "session.idle" => SessionEventData::SessionIdle(SessionIdleData {}),
750        "session.info" => serde_json::from_value(data)
751            .map(SessionEventData::SessionInfo)
752            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
753        "session.model_change" => serde_json::from_value(data)
754            .map(SessionEventData::SessionModelChange)
755            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
756        "session.handoff" => serde_json::from_value(data)
757            .map(SessionEventData::SessionHandoff)
758            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
759        "session.truncation" => serde_json::from_value(data)
760            .map(SessionEventData::SessionTruncation)
761            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
762        "user.message" => serde_json::from_value(data)
763            .map(SessionEventData::UserMessage)
764            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
765        "pending_messages.modified" => {
766            SessionEventData::PendingMessagesModified(PendingMessagesModifiedData {})
767        }
768        "assistant.turn_start" => serde_json::from_value(data)
769            .map(SessionEventData::AssistantTurnStart)
770            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
771        "assistant.intent" => serde_json::from_value(data)
772            .map(SessionEventData::AssistantIntent)
773            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
774        "assistant.reasoning" => serde_json::from_value(data)
775            .map(SessionEventData::AssistantReasoning)
776            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
777        "assistant.reasoning_delta" => serde_json::from_value(data)
778            .map(SessionEventData::AssistantReasoningDelta)
779            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
780        "assistant.message" => serde_json::from_value(data)
781            .map(SessionEventData::AssistantMessage)
782            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
783        "assistant.message_delta" => serde_json::from_value(data)
784            .map(SessionEventData::AssistantMessageDelta)
785            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
786        "assistant.turn_end" => serde_json::from_value(data)
787            .map(SessionEventData::AssistantTurnEnd)
788            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
789        "assistant.usage" => serde_json::from_value(data)
790            .map(SessionEventData::AssistantUsage)
791            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
792        "abort" => serde_json::from_value(data)
793            .map(SessionEventData::Abort)
794            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
795        "tool.user_requested" => serde_json::from_value(data)
796            .map(SessionEventData::ToolUserRequested)
797            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
798        "tool.execution_start" => serde_json::from_value(data)
799            .map(SessionEventData::ToolExecutionStart)
800            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
801        "tool.execution_partial_result" => serde_json::from_value(data)
802            .map(SessionEventData::ToolExecutionPartialResult)
803            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
804        "tool.execution_complete" => serde_json::from_value(data)
805            .map(SessionEventData::ToolExecutionComplete)
806            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
807        "tool.execution_progress" => serde_json::from_value(data)
808            .map(SessionEventData::ToolExecutionProgress)
809            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
810        // Primary wire names (subagent.*) + legacy aliases (custom_agent.*)
811        "subagent.started" | "custom_agent.started" => serde_json::from_value(data)
812            .map(SessionEventData::CustomAgentStarted)
813            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
814        "subagent.completed" | "custom_agent.completed" => serde_json::from_value(data)
815            .map(SessionEventData::CustomAgentCompleted)
816            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
817        "subagent.failed" | "custom_agent.failed" => serde_json::from_value(data)
818            .map(SessionEventData::CustomAgentFailed)
819            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
820        "subagent.selected" | "custom_agent.selected" => serde_json::from_value(data)
821            .map(SessionEventData::CustomAgentSelected)
822            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
823        "hook.start" => serde_json::from_value(data)
824            .map(SessionEventData::HookStart)
825            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
826        "hook.end" => serde_json::from_value(data)
827            .map(SessionEventData::HookEnd)
828            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
829        "system.message" => serde_json::from_value(data)
830            .map(SessionEventData::SystemMessage)
831            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
832        "session.compaction_start" => {
833            SessionEventData::SessionCompactionStart(SessionCompactionStartData {})
834        }
835        "session.compaction_complete" => serde_json::from_value(data)
836            .map(SessionEventData::SessionCompactionComplete)
837            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
838        "session.shutdown" => serde_json::from_value(data)
839            .map(SessionEventData::SessionShutdown)
840            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
841        "session.snapshot_rewind" => serde_json::from_value(data)
842            .map(SessionEventData::SessionSnapshotRewind)
843            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
844        "session.usage_info" => serde_json::from_value(data)
845            .map(SessionEventData::SessionUsageInfo)
846            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
847        "skill.invoked" => serde_json::from_value(data)
848            .map(SessionEventData::SkillInvoked)
849            .unwrap_or_else(|_| SessionEventData::Unknown(serde_json::Value::Null)),
850        // Unknown event type - preserve raw data
851        _ => SessionEventData::Unknown(data),
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use serde_json::json;
859
860    #[test]
861    fn test_parse_assistant_message() {
862        let json = json!({
863            "id": "evt_123",
864            "timestamp": "2024-01-15T10:30:00Z",
865            "type": "assistant.message",
866            "data": {
867                "messageId": "msg_456",
868                "content": "Hello, world!"
869            }
870        });
871
872        let event = SessionEvent::from_json(&json).unwrap();
873        assert_eq!(event.id, "evt_123");
874        assert_eq!(event.event_type, "assistant.message");
875        assert!(event.is_assistant_message());
876
877        let msg = event.as_assistant_message().unwrap();
878        assert_eq!(msg.message_id, "msg_456");
879        assert_eq!(msg.content, "Hello, world!");
880    }
881
882    #[test]
883    fn test_parse_assistant_message_delta() {
884        let json = json!({
885            "id": "evt_124",
886            "timestamp": "2024-01-15T10:30:01Z",
887            "type": "assistant.message_delta",
888            "data": {
889                "messageId": "msg_456",
890                "deltaContent": "Hello"
891            }
892        });
893
894        let event = SessionEvent::from_json(&json).unwrap();
895        assert!(event.is_assistant_message_delta());
896        assert_eq!(event.content(), Some("Hello"));
897    }
898
899    #[test]
900    fn test_parse_session_idle() {
901        let json = json!({
902            "id": "evt_125",
903            "timestamp": "2024-01-15T10:30:02Z",
904            "type": "session.idle",
905            "data": {}
906        });
907
908        let event = SessionEvent::from_json(&json).unwrap();
909        assert!(event.is_session_idle());
910        assert!(event.is_terminal());
911    }
912
913    #[test]
914    fn test_parse_session_error() {
915        let json = json!({
916            "id": "evt_126",
917            "timestamp": "2024-01-15T10:30:03Z",
918            "type": "session.error",
919            "data": {
920                "errorType": "api_error",
921                "message": "Rate limit exceeded"
922            }
923        });
924
925        let event = SessionEvent::from_json(&json).unwrap();
926        assert!(event.is_session_error());
927        assert!(event.is_terminal());
928
929        let err = event.as_session_error().unwrap();
930        assert_eq!(err.error_type, "api_error");
931        assert_eq!(err.message, "Rate limit exceeded");
932    }
933
934    #[test]
935    fn test_parse_tool_execution_complete() {
936        let json = json!({
937            "id": "evt_127",
938            "timestamp": "2024-01-15T10:30:04Z",
939            "type": "tool.execution_complete",
940            "data": {
941                "toolCallId": "call_789",
942                "success": true,
943                "result": {
944                    "content": "Tool output"
945                }
946            }
947        });
948
949        let event = SessionEvent::from_json(&json).unwrap();
950        let tool = event.as_tool_execution_complete().unwrap();
951        assert_eq!(tool.tool_call_id, "call_789");
952        assert!(tool.success);
953        assert_eq!(tool.result.as_ref().unwrap().content, "Tool output");
954    }
955
956    #[test]
957    fn test_parse_unknown_event() {
958        let json = json!({
959            "id": "evt_128",
960            "timestamp": "2024-01-15T10:30:05Z",
961            "type": "future.unknown_event",
962            "data": {
963                "someField": "someValue"
964            }
965        });
966
967        let event = SessionEvent::from_json(&json).unwrap();
968        assert_eq!(event.event_type, "future.unknown_event");
969        assert!(matches!(event.data, SessionEventData::Unknown(_)));
970    }
971
972    #[test]
973    fn test_parse_session_start() {
974        let json = json!({
975            "id": "evt_001",
976            "timestamp": "2024-01-15T10:30:00Z",
977            "type": "session.start",
978            "data": {
979                "sessionId": "sess_123",
980                "version": 1.0,
981                "producer": "copilot-cli",
982                "copilotVersion": "1.0.0",
983                "startTime": "2024-01-15T10:30:00Z"
984            }
985        });
986
987        let event = SessionEvent::from_json(&json).unwrap();
988        if let SessionEventData::SessionStart(data) = &event.data {
989            assert_eq!(data.session_id, "sess_123");
990            assert_eq!(data.producer, "copilot-cli");
991        } else {
992            panic!("Expected SessionStart");
993        }
994    }
995
996    #[test]
997    fn test_event_with_parent_id() {
998        let json = json!({
999            "id": "evt_129",
1000            "timestamp": "2024-01-15T10:30:06Z",
1001            "type": "assistant.message",
1002            "parentId": "evt_128",
1003            "ephemeral": true,
1004            "data": {
1005                "messageId": "msg_789",
1006                "content": "Nested message"
1007            }
1008        });
1009
1010        let event = SessionEvent::from_json(&json).unwrap();
1011        assert_eq!(event.parent_id, Some("evt_128".to_string()));
1012        assert_eq!(event.ephemeral, Some(true));
1013    }
1014
1015    #[test]
1016    fn test_parse_subagent_started() {
1017        let json = json!({
1018            "id": "evt_200",
1019            "timestamp": "2024-01-15T10:30:00Z",
1020            "type": "subagent.started",
1021            "data": {
1022                "toolCallId": "call_1",
1023                "agentName": "test-agent",
1024                "agentDisplayName": "Test Agent",
1025                "agentDescription": "A test agent"
1026            }
1027        });
1028        let event = SessionEvent::from_json(&json).unwrap();
1029        assert!(matches!(
1030            event.data,
1031            SessionEventData::CustomAgentStarted(_)
1032        ));
1033        if let SessionEventData::CustomAgentStarted(data) = &event.data {
1034            assert_eq!(data.agent_name, "test-agent");
1035        }
1036    }
1037
1038    #[test]
1039    fn test_parse_subagent_completed_legacy_alias() {
1040        // Verify legacy custom_agent.* wire names still work
1041        let json = json!({
1042            "id": "evt_201",
1043            "timestamp": "2024-01-15T10:30:00Z",
1044            "type": "custom_agent.completed",
1045            "data": {
1046                "toolCallId": "call_1",
1047                "agentName": "test-agent"
1048            }
1049        });
1050        let event = SessionEvent::from_json(&json).unwrap();
1051        assert!(matches!(
1052            event.data,
1053            SessionEventData::CustomAgentCompleted(_)
1054        ));
1055    }
1056
1057    #[test]
1058    fn test_parse_subagent_all_wire_names() {
1059        for wire_name in &["subagent.failed", "custom_agent.failed"] {
1060            let json = json!({
1061                "id": "evt_202",
1062                "timestamp": "2024-01-15T10:30:00Z",
1063                "type": wire_name,
1064                "data": {
1065                    "toolCallId": "call_1",
1066                    "agentName": "agent",
1067                    "error": "boom"
1068                }
1069            });
1070            let event = SessionEvent::from_json(&json).unwrap();
1071            assert!(
1072                matches!(event.data, SessionEventData::CustomAgentFailed(_)),
1073                "Failed to parse {wire_name}"
1074            );
1075        }
1076    }
1077
1078    #[test]
1079    fn test_parse_session_compaction_start() {
1080        let json = json!({
1081            "id": "evt_300",
1082            "timestamp": "2024-01-15T10:30:00Z",
1083            "type": "session.compaction_start",
1084            "data": {}
1085        });
1086        let event = SessionEvent::from_json(&json).unwrap();
1087        assert!(matches!(
1088            event.data,
1089            SessionEventData::SessionCompactionStart(_)
1090        ));
1091    }
1092
1093    #[test]
1094    fn test_parse_session_compaction_complete() {
1095        let json = json!({
1096            "id": "evt_301",
1097            "timestamp": "2024-01-15T10:30:00Z",
1098            "type": "session.compaction_complete",
1099            "data": {
1100                "success": true,
1101                "preCompactionTokens": 50000.0,
1102                "postCompactionTokens": 10000.0,
1103                "compactionTokensUsed": {
1104                    "input": 100.0,
1105                    "output": 200.0,
1106                    "cachedInput": 50.0
1107                },
1108                "summaryContent": "Session was compacted"
1109            }
1110        });
1111        let event = SessionEvent::from_json(&json).unwrap();
1112        if let SessionEventData::SessionCompactionComplete(data) = &event.data {
1113            assert!(data.success);
1114            assert_eq!(data.pre_compaction_tokens, Some(50000.0));
1115            assert_eq!(data.compaction_tokens_used.as_ref().unwrap().input, 100.0);
1116        } else {
1117            panic!("Expected SessionCompactionComplete");
1118        }
1119    }
1120
1121    #[test]
1122    fn test_parse_session_shutdown() {
1123        let json = json!({
1124            "id": "evt_302",
1125            "timestamp": "2024-01-15T10:30:00Z",
1126            "type": "session.shutdown",
1127            "data": {
1128                "shutdownType": "routine",
1129                "totalPremiumRequests": 5.0,
1130                "totalApiDurationMs": 1200.0,
1131                "sessionStartTime": 1700000000.0,
1132                "codeChanges": {
1133                    "linesAdded": 10.0,
1134                    "linesRemoved": 3.0,
1135                    "filesModified": ["src/main.rs"]
1136                },
1137                "modelMetrics": {},
1138                "currentModel": "gpt-4"
1139            }
1140        });
1141        let event = SessionEvent::from_json(&json).unwrap();
1142        if let SessionEventData::SessionShutdown(data) = &event.data {
1143            assert_eq!(data.shutdown_type, ShutdownType::Routine);
1144            assert_eq!(data.current_model, Some("gpt-4".to_string()));
1145            assert_eq!(data.code_changes.lines_added, 10.0);
1146        } else {
1147            panic!("Expected SessionShutdown");
1148        }
1149    }
1150
1151    #[test]
1152    fn test_parse_session_snapshot_rewind() {
1153        let json = json!({
1154            "id": "evt_303",
1155            "timestamp": "2024-01-15T10:30:00Z",
1156            "type": "session.snapshot_rewind",
1157            "data": {
1158                "upToEventId": "evt_100",
1159                "eventsRemoved": 5.0
1160            }
1161        });
1162        let event = SessionEvent::from_json(&json).unwrap();
1163        if let SessionEventData::SessionSnapshotRewind(data) = &event.data {
1164            assert_eq!(data.up_to_event_id, "evt_100");
1165            assert_eq!(data.events_removed, 5.0);
1166        } else {
1167            panic!("Expected SessionSnapshotRewind");
1168        }
1169    }
1170
1171    #[test]
1172    fn test_parse_session_usage_info() {
1173        let json = json!({
1174            "id": "evt_304",
1175            "timestamp": "2024-01-15T10:30:00Z",
1176            "type": "session.usage_info",
1177            "data": {
1178                "tokenLimit": 100000.0,
1179                "currentTokens": 50000.0,
1180                "messagesLength": 42.0
1181            }
1182        });
1183        let event = SessionEvent::from_json(&json).unwrap();
1184        if let SessionEventData::SessionUsageInfo(data) = &event.data {
1185            assert_eq!(data.token_limit, 100000.0);
1186            assert_eq!(data.current_tokens, 50000.0);
1187        } else {
1188            panic!("Expected SessionUsageInfo");
1189        }
1190    }
1191
1192    #[test]
1193    fn test_parse_tool_execution_progress() {
1194        let json = json!({
1195            "id": "evt_305",
1196            "timestamp": "2024-01-15T10:30:00Z",
1197            "type": "tool.execution_progress",
1198            "data": {
1199                "toolCallId": "call_100",
1200                "progressMessage": "Processing file 3 of 10..."
1201            }
1202        });
1203        let event = SessionEvent::from_json(&json).unwrap();
1204        if let SessionEventData::ToolExecutionProgress(data) = &event.data {
1205            assert_eq!(data.tool_call_id, "call_100");
1206            assert_eq!(data.progress_message, "Processing file 3 of 10...");
1207        } else {
1208            panic!("Expected ToolExecutionProgress");
1209        }
1210    }
1211
1212    #[test]
1213    fn test_parse_skill_invoked() {
1214        let json = json!({
1215            "id": "evt_306",
1216            "timestamp": "2024-01-15T10:30:00Z",
1217            "type": "skill.invoked",
1218            "data": {
1219                "name": "code-review",
1220                "path": "/skills/code-review",
1221                "content": "Review this code",
1222                "allowedTools": ["read_file", "search"]
1223            }
1224        });
1225        let event = SessionEvent::from_json(&json).unwrap();
1226        if let SessionEventData::SkillInvoked(data) = &event.data {
1227            assert_eq!(data.name, "code-review");
1228            assert_eq!(data.allowed_tools.as_ref().unwrap().len(), 2);
1229        } else {
1230            panic!("Expected SkillInvoked");
1231        }
1232    }
1233
1234    #[test]
1235    fn test_session_error_with_code_and_provider_call_id() {
1236        let json = json!({
1237            "id": "evt_err",
1238            "timestamp": "2024-01-15T10:30:00Z",
1239            "type": "session.error",
1240            "data": {
1241                "errorType": "provider_error",
1242                "message": "Rate limited",
1243                "code": 429.0,
1244                "providerCallId": "call-abc-123"
1245            }
1246        });
1247        let event = SessionEvent::from_json(&json).unwrap();
1248        if let SessionEventData::SessionError(data) = &event.data {
1249            assert_eq!(data.error_type, "provider_error");
1250            assert_eq!(data.code, Some(429.0));
1251            assert_eq!(data.provider_call_id.as_deref(), Some("call-abc-123"));
1252        } else {
1253            panic!("Expected SessionError");
1254        }
1255    }
1256
1257    #[test]
1258    fn test_tool_execution_complete_with_mcp_fields() {
1259        let json = json!({
1260            "id": "evt_mcp",
1261            "timestamp": "2024-01-15T10:30:00Z",
1262            "type": "tool.execution_complete",
1263            "data": {
1264                "toolCallId": "call-1",
1265                "success": true,
1266                "mcpServerName": "my-server",
1267                "mcpToolName": "read_file"
1268            }
1269        });
1270        let event = SessionEvent::from_json(&json).unwrap();
1271        if let SessionEventData::ToolExecutionComplete(data) = &event.data {
1272            assert_eq!(data.mcp_server_name.as_deref(), Some("my-server"));
1273            assert_eq!(data.mcp_tool_name.as_deref(), Some("read_file"));
1274        } else {
1275            panic!("Expected ToolExecutionComplete");
1276        }
1277    }
1278
1279    #[test]
1280    fn test_session_start_data_optional_fields() {
1281        // All fields missing should still parse with defaults
1282        let json = json!({
1283            "id": "evt_start",
1284            "timestamp": "2024-01-15T10:30:00Z",
1285            "type": "session.start",
1286            "data": {}
1287        });
1288        let event = SessionEvent::from_json(&json).unwrap();
1289        if let SessionEventData::SessionStart(data) = &event.data {
1290            assert_eq!(data.session_id, "");
1291            assert_eq!(data.version, 0.0);
1292            assert_eq!(data.producer, "");
1293        } else {
1294            panic!("Expected SessionStart");
1295        }
1296    }
1297
1298    #[test]
1299    fn test_unknown_event_type_handled_gracefully() {
1300        let json = json!({
1301            "id": "evt_unknown",
1302            "timestamp": "2025-01-01T00:00:00Z",
1303            "type": "some.future.event.type",
1304            "data": {"someField": "someValue"}
1305        });
1306        // Parsing an unknown event type should not panic
1307        let raw: RawSessionEvent = serde_json::from_value(json.clone()).unwrap();
1308        assert_eq!(raw.event_type, "some.future.event.type");
1309
1310        // It should also parse into a SessionEvent with Unknown data
1311        let event = SessionEvent::from_json(&json).unwrap();
1312        assert_eq!(event.event_type, "some.future.event.type");
1313        assert!(matches!(event.data, SessionEventData::Unknown(_)));
1314    }
1315
1316    #[test]
1317    fn test_session_shutdown_event_parsed() {
1318        let json = json!({
1319            "id": "evt_shutdown",
1320            "timestamp": "2025-01-01T00:00:00Z",
1321            "type": "session.shutdown",
1322            "data": {
1323                "shutdownType": "routine",
1324                "reason": "user requested"
1325            }
1326        });
1327        let raw: RawSessionEvent = serde_json::from_value(json.clone()).unwrap();
1328        assert_eq!(raw.event_type, "session.shutdown");
1329
1330        let event = SessionEvent::from_json(&json).unwrap();
1331        assert_eq!(event.event_type, "session.shutdown");
1332    }
1333
1334    #[test]
1335    fn test_session_usage_info_recognized() {
1336        let json = json!({
1337            "id": "evt_usage",
1338            "timestamp": "2025-01-01T00:00:00Z",
1339            "type": "session.usage_info",
1340            "data": {}
1341        });
1342        let raw: RawSessionEvent = serde_json::from_value(json.clone()).unwrap();
1343        assert_eq!(raw.event_type, "session.usage_info");
1344
1345        let event = SessionEvent::from_json(&json).unwrap();
1346        assert_eq!(event.event_type, "session.usage_info");
1347    }
1348}