Skip to main content

a3s_code_core/hooks/
events.rs

1//! Hook Event Types
2//!
3//! Defines all event types that can trigger hooks.
4
5use serde::{Deserialize, Serialize};
6
7/// Hook event types
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum HookEventType {
11    /// Before tool execution
12    PreToolUse,
13    /// After tool execution
14    PostToolUse,
15    /// Before interactive permission is requested
16    PermissionRequest,
17    /// Before context compaction
18    PreCompact,
19    /// After context compaction
20    PostCompact,
21    /// Before LLM generation
22    GenerateStart,
23    /// After LLM generation
24    GenerateEnd,
25    /// When session is created
26    SessionStart,
27    /// When session is destroyed
28    SessionEnd,
29    /// When a skill is loaded
30    SkillLoad,
31    /// When a skill is unloaded
32    SkillUnload,
33    /// Before prompt augmentation (can modify prompt)
34    PrePrompt,
35    /// After LLM response is processed, before returning to user
36    PostResponse,
37    /// When an error occurs (tool failure, LLM error, etc.)
38    OnError,
39    // === New harness points ===
40    /// Before context perception (model needs workspace knowledge)
41    PreContextPerception,
42    /// After context perception
43    PostContextPerception,
44
45    /// When an operation succeeds (mirrors OnError for success case)
46    OnSuccess,
47
48    /// Before memory recall (model needs to retrieve from memory)
49    PreMemoryRecall,
50    /// After memory recall completes
51    PostMemoryRecall,
52
53    /// Before task planning/decomposition
54    PrePlanning,
55    /// After planning completes
56    PostPlanning,
57
58    /// Before reasoning (CoT/ToT start)
59    PreReasoning,
60    /// After reasoning completes
61    PostReasoning,
62
63    /// When rate limit is triggered
64    OnRateLimit,
65
66    /// When user confirmation is needed
67    OnConfirmation,
68
69    /// Intent detection - detect user intent from prompt (blocking)
70    IntentDetection,
71}
72
73impl std::fmt::Display for HookEventType {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            HookEventType::PreToolUse => write!(f, "pre_tool_use"),
77            HookEventType::PostToolUse => write!(f, "post_tool_use"),
78            HookEventType::PermissionRequest => write!(f, "permission_request"),
79            HookEventType::PreCompact => write!(f, "pre_compact"),
80            HookEventType::PostCompact => write!(f, "post_compact"),
81            HookEventType::GenerateStart => write!(f, "generate_start"),
82            HookEventType::GenerateEnd => write!(f, "generate_end"),
83            HookEventType::SessionStart => write!(f, "session_start"),
84            HookEventType::SessionEnd => write!(f, "session_end"),
85            HookEventType::SkillLoad => write!(f, "skill_load"),
86            HookEventType::SkillUnload => write!(f, "skill_unload"),
87            HookEventType::PrePrompt => write!(f, "pre_prompt"),
88            HookEventType::PostResponse => write!(f, "post_response"),
89            HookEventType::OnError => write!(f, "on_error"),
90            // New harness points
91            HookEventType::PreContextPerception => write!(f, "pre_context_perception"),
92            HookEventType::PostContextPerception => write!(f, "post_context_perception"),
93            HookEventType::OnSuccess => write!(f, "on_success"),
94            HookEventType::PreMemoryRecall => write!(f, "pre_memory_recall"),
95            HookEventType::PostMemoryRecall => write!(f, "post_memory_recall"),
96            HookEventType::PrePlanning => write!(f, "pre_planning"),
97            HookEventType::PostPlanning => write!(f, "post_planning"),
98            HookEventType::PreReasoning => write!(f, "pre_reasoning"),
99            HookEventType::PostReasoning => write!(f, "post_reasoning"),
100            HookEventType::OnRateLimit => write!(f, "on_rate_limit"),
101            HookEventType::OnConfirmation => write!(f, "on_confirmation"),
102            HookEventType::IntentDetection => write!(f, "intent_detection"),
103        }
104    }
105}
106
107/// Tool execution result data
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ToolResultData {
110    /// Whether execution succeeded
111    pub success: bool,
112    /// Tool output
113    pub output: String,
114    /// Exit code (for shell commands)
115    pub exit_code: Option<i32>,
116    /// Execution duration in milliseconds
117    pub duration_ms: u64,
118}
119
120/// Pre-tool-use event payload
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct PreToolUseEvent {
123    /// Session ID
124    pub session_id: String,
125    /// Tool name
126    pub tool: String,
127    /// Tool arguments
128    pub args: serde_json::Value,
129    /// Working directory
130    pub working_directory: String,
131    /// Recent tools executed (for context)
132    pub recent_tools: Vec<String>,
133}
134
135/// Post-tool-use event payload
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct PostToolUseEvent {
138    /// Session ID
139    pub session_id: String,
140    /// Tool name
141    pub tool: String,
142    /// Tool arguments
143    pub args: serde_json::Value,
144    /// Execution result
145    pub result: ToolResultData,
146}
147
148/// Permission-request event payload, fired before the host confirmation UI.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct PermissionRequestEvent {
151    pub session_id: String,
152    pub tool_id: String,
153    pub tool: String,
154    pub args: serde_json::Value,
155}
156
157/// Pre-compaction event payload.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct PreCompactEvent {
160    pub session_id: String,
161    pub message_count: usize,
162    pub used_tokens: usize,
163    pub max_tokens: usize,
164}
165
166/// Post-compaction event payload.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct PostCompactEvent {
169    pub session_id: String,
170    pub message_count_before: usize,
171    pub message_count_after: usize,
172    pub summary_generated: bool,
173}
174
175/// Generate start event payload
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct GenerateStartEvent {
178    /// Session ID
179    pub session_id: String,
180    /// User prompt
181    pub prompt: String,
182    /// System prompt (if any)
183    pub system_prompt: Option<String>,
184    /// Model provider
185    pub model_provider: String,
186    /// Model name
187    pub model_name: String,
188    /// Available tools
189    pub available_tools: Vec<String>,
190}
191
192/// Generate end event payload
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct GenerateEndEvent {
195    /// Session ID
196    pub session_id: String,
197    /// User prompt
198    pub prompt: String,
199    /// Response text
200    pub response_text: String,
201    /// Tool calls made
202    pub tool_calls: Vec<ToolCallInfo>,
203    /// Token usage
204    pub usage: TokenUsageInfo,
205    /// Duration in milliseconds
206    pub duration_ms: u64,
207}
208
209/// Tool call information
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct ToolCallInfo {
212    /// Tool name
213    pub name: String,
214    /// Tool arguments
215    pub args: serde_json::Value,
216}
217
218/// Token usage information
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct TokenUsageInfo {
221    /// Prompt tokens
222    pub prompt_tokens: i32,
223    /// Completion tokens
224    pub completion_tokens: i32,
225    /// Total tokens
226    pub total_tokens: i32,
227}
228
229/// Session start event payload
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct SessionStartEvent {
232    /// Session ID
233    pub session_id: String,
234    /// System prompt (if any)
235    pub system_prompt: Option<String>,
236    /// Model configuration
237    pub model_provider: String,
238    pub model_name: String,
239}
240
241/// Session end event payload
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct SessionEndEvent {
244    /// Session ID
245    pub session_id: String,
246    /// Total token usage
247    pub total_tokens: i32,
248    /// Total tool calls
249    pub total_tool_calls: i32,
250    /// Session duration in milliseconds
251    pub duration_ms: u64,
252}
253
254/// Skill load event payload
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct SkillLoadEvent {
257    /// Skill name
258    pub skill_name: String,
259    /// Tool names loaded from the skill
260    pub tool_names: Vec<String>,
261    /// Skill version (if available)
262    pub version: Option<String>,
263    /// Skill description (if available)
264    pub description: Option<String>,
265    /// Timestamp when skill was loaded (Unix milliseconds)
266    pub loaded_at: i64,
267}
268
269/// Skill unload event payload
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct SkillUnloadEvent {
272    /// Skill name
273    pub skill_name: String,
274    /// Tool names that were unloaded
275    pub tool_names: Vec<String>,
276    /// How long the skill was loaded (milliseconds)
277    pub duration_ms: u64,
278}
279
280/// Pre-prompt event payload (fired before prompt augmentation)
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct PrePromptEvent {
283    /// Session ID
284    pub session_id: String,
285    /// User prompt text
286    pub prompt: String,
287    /// Current system prompt (if any)
288    pub system_prompt: Option<String>,
289    /// Number of messages in conversation history
290    pub message_count: usize,
291}
292
293/// Post-response event payload (fired after LLM response is processed)
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct PostResponseEvent {
296    /// Session ID
297    pub session_id: String,
298    /// Final response text
299    pub response_text: String,
300    /// Number of tool calls made during this turn
301    pub tool_calls_count: usize,
302    /// Token usage
303    pub usage: TokenUsageInfo,
304    /// Total duration in milliseconds
305    pub duration_ms: u64,
306}
307
308/// Error type classification for OnError events
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(rename_all = "snake_case")]
311pub enum ErrorType {
312    /// Tool execution failed
313    ToolFailure,
314    /// LLM API call failed
315    LlmFailure,
316    /// Permission denied
317    PermissionDenied,
318    /// Timeout
319    Timeout,
320    /// Other error
321    Other,
322}
323
324impl std::fmt::Display for ErrorType {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        match self {
327            ErrorType::ToolFailure => write!(f, "tool_failure"),
328            ErrorType::LlmFailure => write!(f, "llm_failure"),
329            ErrorType::PermissionDenied => write!(f, "permission_denied"),
330            ErrorType::Timeout => write!(f, "timeout"),
331            ErrorType::Other => write!(f, "other"),
332        }
333    }
334}
335
336/// On-error event payload
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct OnErrorEvent {
339    /// Session ID
340    pub session_id: String,
341    /// Error classification
342    pub error_type: ErrorType,
343    /// Error message
344    pub error_message: String,
345    /// Additional context (e.g., tool name, model name)
346    pub context: serde_json::Value,
347}
348
349// ============================================================================
350// New Driving Point Payloads
351// ============================================================================
352
353/// Pre-context-perception event payload
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct PreContextPerceptionEvent {
356    pub session_id: String,
357    pub intent: String,
358    pub target_type: String,
359    pub target_name: String,
360    pub domain: String,
361    pub query: Option<String>,
362    pub working_directory: String,
363    pub urgency: String,
364}
365
366/// Post-context-perception event payload
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct PostContextPerceptionEvent {
369    pub session_id: String,
370    pub intent: String,
371    pub target_type: String,
372    pub success: bool,
373    pub facts_retrieved: usize,
374    pub files_retrieved: usize,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub error: Option<String>,
377}
378
379/// On-success event payload (mirrors OnError for success)
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct OnSuccessEvent {
382    /// Session ID
383    pub session_id: String,
384    /// Action type that succeeded
385    pub action_type: String,
386    /// Summary of the successful action
387    pub action_summary: String,
388    /// Duration in milliseconds
389    pub duration_ms: u64,
390}
391
392/// Pre-memory-recall event payload
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct PreMemoryRecallEvent {
395    pub session_id: String,
396    /// Query or intent for recall
397    pub query: String,
398    /// Memory type (e.g., "semantic", "episodic", "working")
399    pub memory_type: String,
400    /// Maximum results to retrieve
401    pub max_results: usize,
402    /// Current working directory
403    pub working_directory: String,
404}
405
406/// Post-memory-recall event payload
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct PostMemoryRecallEvent {
409    pub session_id: String,
410    pub query: String,
411    pub memory_type: String,
412    pub facts_retrieved: usize,
413    pub success: bool,
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub error: Option<String>,
416}
417
418/// Planning strategy type
419#[derive(Debug, Clone, Serialize, Deserialize)]
420#[serde(rename_all = "snake_case")]
421pub enum PlanningStrategy {
422    None,
423    StepByStep,
424    TreeOfThoughts,
425    GraphPlanning,
426    Custom(String),
427}
428
429/// Pre-planning event payload
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct PrePlanningEvent {
432    pub session_id: String,
433    /// Task description to plan
434    pub task_description: String,
435    /// Available planning strategies
436    pub available_strategies: Vec<PlanningStrategy>,
437    /// Constraints or requirements
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub constraints: Option<serde_json::Value>,
440}
441
442/// Post-planning event payload
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct PostPlanningEvent {
445    pub session_id: String,
446    pub task_description: String,
447    pub strategy_used: PlanningStrategy,
448    /// Generated subtasks or plan steps
449    pub subtasks: Vec<String>,
450    pub success: bool,
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub error: Option<String>,
453}
454
455/// Reasoning type
456#[derive(Debug, Clone, Serialize, Deserialize)]
457#[serde(rename_all = "snake_case")]
458pub enum ReasoningType {
459    ChainOfThought,
460    TreeOfThoughts,
461    ReAct,
462    Reflexion,
463    Other(String),
464}
465
466/// Pre-reasoning event payload
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct PreReasoningEvent {
469    pub session_id: String,
470    /// Type of reasoning being performed
471    pub reasoning_type: ReasoningType,
472    /// Problem or question to reason about
473    pub problem_statement: String,
474    /// Available hints or context
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub hints: Option<Vec<String>>,
477}
478
479/// Post-reasoning event payload
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct PostReasoningEvent {
482    pub session_id: String,
483    pub reasoning_type: ReasoningType,
484    pub conclusion: String,
485    pub steps_count: usize,
486    pub success: bool,
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub error: Option<String>,
489}
490
491/// Rate limit type
492#[derive(Debug, Clone, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum RateLimitType {
495    LlmTokenLimit,
496    LlmRequestLimit,
497    ApiRequestLimit,
498    ToolExecutionLimit,
499    Custom(String),
500}
501
502/// On-rate-limit event payload
503#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct OnRateLimitEvent {
505    pub session_id: String,
506    /// Type of rate limit
507    pub limit_type: RateLimitType,
508    /// Retry after milliseconds (suggested)
509    pub retry_after_ms: u64,
510    /// Current usage information
511    pub current_usage: String,
512}
513
514/// Confirmation type
515#[derive(Debug, Clone, Serialize, Deserialize)]
516#[serde(rename_all = "snake_case")]
517pub enum ConfirmationType {
518    SafetyConfirm,
519    UserConfirm,
520    CostConfirm,
521    Custom(String),
522}
523
524/// On-confirmation event payload
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct OnConfirmationEvent {
527    pub session_id: String,
528    /// Type of confirmation needed
529    pub confirmation_type: ConfirmationType,
530    /// Message to show to user
531    pub message: String,
532    /// Options to present (if any)
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub options: Option<Vec<String>>,
535}
536
537/// Intent detection event payload
538#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct IntentDetectionEvent {
540    pub session_id: String,
541    pub prompt: String,
542    pub workspace: String,
543    /// Optional language hint auto-detected from input
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub language_hint: Option<String>,
546}
547
548/// Unified hook event enum
549#[derive(Debug, Clone, Serialize, Deserialize)]
550#[serde(tag = "event_type", content = "payload")]
551pub enum HookEvent {
552    #[serde(rename = "pre_tool_use")]
553    PreToolUse(PreToolUseEvent),
554    #[serde(rename = "post_tool_use")]
555    PostToolUse(PostToolUseEvent),
556    #[serde(rename = "permission_request")]
557    PermissionRequest(PermissionRequestEvent),
558    #[serde(rename = "pre_compact")]
559    PreCompact(PreCompactEvent),
560    #[serde(rename = "post_compact")]
561    PostCompact(PostCompactEvent),
562    #[serde(rename = "generate_start")]
563    GenerateStart(GenerateStartEvent),
564    #[serde(rename = "generate_end")]
565    GenerateEnd(GenerateEndEvent),
566    #[serde(rename = "session_start")]
567    SessionStart(SessionStartEvent),
568    #[serde(rename = "session_end")]
569    SessionEnd(SessionEndEvent),
570    #[serde(rename = "skill_load")]
571    SkillLoad(SkillLoadEvent),
572    #[serde(rename = "skill_unload")]
573    SkillUnload(SkillUnloadEvent),
574    #[serde(rename = "pre_prompt")]
575    PrePrompt(PrePromptEvent),
576    #[serde(rename = "post_response")]
577    PostResponse(PostResponseEvent),
578    #[serde(rename = "on_error")]
579    OnError(OnErrorEvent),
580    // New harness points
581    #[serde(rename = "pre_context_perception")]
582    PreContextPerception(PreContextPerceptionEvent),
583    #[serde(rename = "post_context_perception")]
584    PostContextPerception(PostContextPerceptionEvent),
585    #[serde(rename = "on_success")]
586    OnSuccess(OnSuccessEvent),
587    #[serde(rename = "pre_memory_recall")]
588    PreMemoryRecall(PreMemoryRecallEvent),
589    #[serde(rename = "post_memory_recall")]
590    PostMemoryRecall(PostMemoryRecallEvent),
591    #[serde(rename = "pre_planning")]
592    PrePlanning(PrePlanningEvent),
593    #[serde(rename = "post_planning")]
594    PostPlanning(PostPlanningEvent),
595    #[serde(rename = "pre_reasoning")]
596    PreReasoning(PreReasoningEvent),
597    #[serde(rename = "post_reasoning")]
598    PostReasoning(PostReasoningEvent),
599    #[serde(rename = "on_rate_limit")]
600    OnRateLimit(OnRateLimitEvent),
601    #[serde(rename = "on_confirmation")]
602    OnConfirmation(OnConfirmationEvent),
603    #[serde(rename = "intent_detection")]
604    IntentDetection(IntentDetectionEvent),
605}
606
607impl HookEvent {
608    /// Get the event type
609    pub fn event_type(&self) -> HookEventType {
610        match self {
611            HookEvent::PreToolUse(_) => HookEventType::PreToolUse,
612            HookEvent::PostToolUse(_) => HookEventType::PostToolUse,
613            HookEvent::PermissionRequest(_) => HookEventType::PermissionRequest,
614            HookEvent::PreCompact(_) => HookEventType::PreCompact,
615            HookEvent::PostCompact(_) => HookEventType::PostCompact,
616            HookEvent::GenerateStart(_) => HookEventType::GenerateStart,
617            HookEvent::GenerateEnd(_) => HookEventType::GenerateEnd,
618            HookEvent::SessionStart(_) => HookEventType::SessionStart,
619            HookEvent::SessionEnd(_) => HookEventType::SessionEnd,
620            HookEvent::SkillLoad(_) => HookEventType::SkillLoad,
621            HookEvent::SkillUnload(_) => HookEventType::SkillUnload,
622            HookEvent::PrePrompt(_) => HookEventType::PrePrompt,
623            HookEvent::PostResponse(_) => HookEventType::PostResponse,
624            HookEvent::OnError(_) => HookEventType::OnError,
625            // New harness points
626            HookEvent::PreContextPerception(_) => HookEventType::PreContextPerception,
627            HookEvent::PostContextPerception(_) => HookEventType::PostContextPerception,
628            HookEvent::OnSuccess(_) => HookEventType::OnSuccess,
629            HookEvent::PreMemoryRecall(_) => HookEventType::PreMemoryRecall,
630            HookEvent::PostMemoryRecall(_) => HookEventType::PostMemoryRecall,
631            HookEvent::PrePlanning(_) => HookEventType::PrePlanning,
632            HookEvent::PostPlanning(_) => HookEventType::PostPlanning,
633            HookEvent::PreReasoning(_) => HookEventType::PreReasoning,
634            HookEvent::PostReasoning(_) => HookEventType::PostReasoning,
635            HookEvent::OnRateLimit(_) => HookEventType::OnRateLimit,
636            HookEvent::OnConfirmation(_) => HookEventType::OnConfirmation,
637            HookEvent::IntentDetection(_) => HookEventType::IntentDetection,
638        }
639    }
640
641    /// Get the session ID (returns empty string for skill events which are global)
642    pub fn session_id(&self) -> &str {
643        match self {
644            HookEvent::PreToolUse(e) => &e.session_id,
645            HookEvent::PostToolUse(e) => &e.session_id,
646            HookEvent::PermissionRequest(e) => &e.session_id,
647            HookEvent::PreCompact(e) => &e.session_id,
648            HookEvent::PostCompact(e) => &e.session_id,
649            HookEvent::GenerateStart(e) => &e.session_id,
650            HookEvent::GenerateEnd(e) => &e.session_id,
651            HookEvent::SessionStart(e) => &e.session_id,
652            HookEvent::SessionEnd(e) => &e.session_id,
653            HookEvent::PrePrompt(e) => &e.session_id,
654            HookEvent::PostResponse(e) => &e.session_id,
655            HookEvent::OnError(e) => &e.session_id,
656            // New harness points
657            HookEvent::PreContextPerception(e) => &e.session_id,
658            HookEvent::PostContextPerception(e) => &e.session_id,
659            HookEvent::OnSuccess(e) => &e.session_id,
660            HookEvent::PreMemoryRecall(e) => &e.session_id,
661            HookEvent::PostMemoryRecall(e) => &e.session_id,
662            HookEvent::PrePlanning(e) => &e.session_id,
663            HookEvent::PostPlanning(e) => &e.session_id,
664            HookEvent::PreReasoning(e) => &e.session_id,
665            HookEvent::PostReasoning(e) => &e.session_id,
666            HookEvent::OnRateLimit(e) => &e.session_id,
667            HookEvent::OnConfirmation(e) => &e.session_id,
668            HookEvent::IntentDetection(e) => &e.session_id,
669            // Skill events are global (not session-specific)
670            HookEvent::SkillLoad(_) => "",
671            HookEvent::SkillUnload(_) => "",
672        }
673    }
674
675    /// Get the tool name (for tool events)
676    pub fn tool_name(&self) -> Option<&str> {
677        match self {
678            HookEvent::PreToolUse(e) => Some(&e.tool),
679            HookEvent::PostToolUse(e) => Some(&e.tool),
680            _ => None,
681        }
682    }
683
684    /// Get the tool args (for tool events)
685    pub fn tool_args(&self) -> Option<&serde_json::Value> {
686        match self {
687            HookEvent::PreToolUse(e) => Some(&e.args),
688            HookEvent::PostToolUse(e) => Some(&e.args),
689            _ => None,
690        }
691    }
692
693    /// Get the skill name (for skill events)
694    pub fn skill_name(&self) -> Option<&str> {
695        match self {
696            HookEvent::SkillLoad(e) => Some(&e.skill_name),
697            HookEvent::SkillUnload(e) => Some(&e.skill_name),
698            _ => None,
699        }
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn test_hook_event_type_display() {
709        assert_eq!(HookEventType::PreToolUse.to_string(), "pre_tool_use");
710        assert_eq!(HookEventType::PostToolUse.to_string(), "post_tool_use");
711        assert_eq!(HookEventType::GenerateStart.to_string(), "generate_start");
712        assert_eq!(HookEventType::GenerateEnd.to_string(), "generate_end");
713        assert_eq!(HookEventType::SessionStart.to_string(), "session_start");
714        assert_eq!(HookEventType::SessionEnd.to_string(), "session_end");
715        assert_eq!(HookEventType::SkillLoad.to_string(), "skill_load");
716        assert_eq!(HookEventType::SkillUnload.to_string(), "skill_unload");
717    }
718
719    #[test]
720    fn test_pre_tool_use_event() {
721        let event = PreToolUseEvent {
722            session_id: "session-1".to_string(),
723            tool: "Bash".to_string(),
724            args: serde_json::json!({"command": "echo hello"}),
725            working_directory: "/workspace".to_string(),
726            recent_tools: vec!["Read".to_string()],
727        };
728
729        assert_eq!(event.session_id, "session-1");
730        assert_eq!(event.tool, "Bash");
731    }
732
733    #[test]
734    fn test_post_tool_use_event() {
735        let event = PostToolUseEvent {
736            session_id: "session-1".to_string(),
737            tool: "Bash".to_string(),
738            args: serde_json::json!({"command": "echo hello"}),
739            result: ToolResultData {
740                success: true,
741                output: "hello\n".to_string(),
742                exit_code: Some(0),
743                duration_ms: 50,
744            },
745        };
746
747        assert!(event.result.success);
748        assert_eq!(event.result.exit_code, Some(0));
749    }
750
751    #[test]
752    fn test_hook_event_type() {
753        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
754            session_id: "s1".to_string(),
755            tool: "Bash".to_string(),
756            args: serde_json::json!({}),
757            working_directory: "/".to_string(),
758            recent_tools: vec![],
759        });
760
761        assert_eq!(pre_tool.event_type(), HookEventType::PreToolUse);
762        assert_eq!(pre_tool.session_id(), "s1");
763        assert_eq!(pre_tool.tool_name(), Some("Bash"));
764    }
765
766    #[test]
767    fn test_hook_event_serialization() {
768        let event = HookEvent::PreToolUse(PreToolUseEvent {
769            session_id: "s1".to_string(),
770            tool: "Bash".to_string(),
771            args: serde_json::json!({"command": "ls"}),
772            working_directory: "/workspace".to_string(),
773            recent_tools: vec![],
774        });
775
776        let json = serde_json::to_string(&event).unwrap();
777        assert!(json.contains("pre_tool_use"));
778        assert!(json.contains("Bash"));
779
780        // Deserialize back
781        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
782        assert_eq!(parsed.event_type(), HookEventType::PreToolUse);
783    }
784
785    #[test]
786    fn test_generate_events() {
787        let start = GenerateStartEvent {
788            session_id: "s1".to_string(),
789            prompt: "Hello".to_string(),
790            system_prompt: Some("You are helpful".to_string()),
791            model_provider: "anthropic".to_string(),
792            model_name: "claude-3".to_string(),
793            available_tools: vec!["Bash".to_string(), "Read".to_string()],
794        };
795
796        let end = GenerateEndEvent {
797            session_id: "s1".to_string(),
798            prompt: "Hello".to_string(),
799            response_text: "Hi there!".to_string(),
800            tool_calls: vec![],
801            usage: TokenUsageInfo {
802                prompt_tokens: 10,
803                completion_tokens: 5,
804                total_tokens: 15,
805            },
806            duration_ms: 500,
807        };
808
809        assert_eq!(start.prompt, "Hello");
810        assert_eq!(end.response_text, "Hi there!");
811        assert_eq!(end.usage.total_tokens, 15);
812    }
813
814    #[test]
815    fn test_session_events() {
816        let start = SessionStartEvent {
817            session_id: "s1".to_string(),
818            system_prompt: Some("System".to_string()),
819            model_provider: "anthropic".to_string(),
820            model_name: "claude-3".to_string(),
821        };
822
823        let end = SessionEndEvent {
824            session_id: "s1".to_string(),
825            total_tokens: 1000,
826            total_tool_calls: 5,
827            duration_ms: 60000,
828        };
829
830        let start_event = HookEvent::SessionStart(start);
831        let end_event = HookEvent::SessionEnd(end);
832
833        assert_eq!(start_event.event_type(), HookEventType::SessionStart);
834        assert_eq!(end_event.event_type(), HookEventType::SessionEnd);
835        assert!(start_event.tool_name().is_none());
836    }
837
838    #[test]
839    fn test_skill_load_event() {
840        let event = SkillLoadEvent {
841            skill_name: "test-skill".to_string(),
842            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
843            version: Some("1.0.0".to_string()),
844            description: Some("A test skill".to_string()),
845            loaded_at: 1234567890,
846        };
847
848        assert_eq!(event.skill_name, "test-skill");
849        assert_eq!(event.tool_names.len(), 2);
850        assert_eq!(event.version, Some("1.0.0".to_string()));
851        assert_eq!(event.loaded_at, 1234567890);
852    }
853
854    #[test]
855    fn test_skill_unload_event() {
856        let event = SkillUnloadEvent {
857            skill_name: "test-skill".to_string(),
858            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
859            duration_ms: 60000,
860        };
861
862        assert_eq!(event.skill_name, "test-skill");
863        assert_eq!(event.tool_names.len(), 2);
864        assert_eq!(event.duration_ms, 60000);
865    }
866
867    #[test]
868    fn test_hook_event_skill_name() {
869        let load_event = HookEvent::SkillLoad(SkillLoadEvent {
870            skill_name: "my-skill".to_string(),
871            tool_names: vec!["tool1".to_string()],
872            version: None,
873            description: None,
874            loaded_at: 0,
875        });
876
877        let unload_event = HookEvent::SkillUnload(SkillUnloadEvent {
878            skill_name: "my-skill".to_string(),
879            tool_names: vec!["tool1".to_string()],
880            duration_ms: 1000,
881        });
882
883        assert_eq!(load_event.event_type(), HookEventType::SkillLoad);
884        assert_eq!(load_event.skill_name(), Some("my-skill"));
885        assert_eq!(load_event.session_id(), ""); // Skills are global
886
887        assert_eq!(unload_event.event_type(), HookEventType::SkillUnload);
888        assert_eq!(unload_event.skill_name(), Some("my-skill"));
889        assert_eq!(unload_event.session_id(), ""); // Skills are global
890
891        // Non-skill events return None for skill_name
892        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
893            session_id: "s1".to_string(),
894            tool: "Bash".to_string(),
895            args: serde_json::json!({}),
896            working_directory: "/".to_string(),
897            recent_tools: vec![],
898        });
899        assert!(pre_tool.skill_name().is_none());
900    }
901
902    #[test]
903    fn test_skill_event_serialization() {
904        let event = HookEvent::SkillLoad(SkillLoadEvent {
905            skill_name: "test-skill".to_string(),
906            tool_names: vec!["tool1".to_string()],
907            version: Some("1.0.0".to_string()),
908            description: None,
909            loaded_at: 1234567890,
910        });
911
912        let json = serde_json::to_string(&event).unwrap();
913        assert!(json.contains("skill_load"));
914        assert!(json.contains("test-skill"));
915        assert!(json.contains("1.0.0"));
916
917        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
918        assert_eq!(parsed.event_type(), HookEventType::SkillLoad);
919        assert_eq!(parsed.skill_name(), Some("test-skill"));
920    }
921
922    #[test]
923    fn test_hook_event_type_display_new_variants() {
924        assert_eq!(HookEventType::PrePrompt.to_string(), "pre_prompt");
925        assert_eq!(HookEventType::PostResponse.to_string(), "post_response");
926        assert_eq!(HookEventType::OnError.to_string(), "on_error");
927    }
928
929    #[test]
930    fn test_pre_prompt_event() {
931        let event = PrePromptEvent {
932            session_id: "s1".to_string(),
933            prompt: "Fix the bug".to_string(),
934            system_prompt: Some("You are helpful".to_string()),
935            message_count: 5,
936        };
937
938        assert_eq!(event.session_id, "s1");
939        assert_eq!(event.prompt, "Fix the bug");
940        assert_eq!(event.message_count, 5);
941
942        let hook_event = HookEvent::PrePrompt(event);
943        assert_eq!(hook_event.event_type(), HookEventType::PrePrompt);
944        assert_eq!(hook_event.session_id(), "s1");
945        assert!(hook_event.tool_name().is_none());
946        assert!(hook_event.skill_name().is_none());
947    }
948
949    #[test]
950    fn test_post_response_event() {
951        let event = PostResponseEvent {
952            session_id: "s1".to_string(),
953            response_text: "Done!".to_string(),
954            tool_calls_count: 3,
955            usage: TokenUsageInfo {
956                prompt_tokens: 100,
957                completion_tokens: 50,
958                total_tokens: 150,
959            },
960            duration_ms: 2000,
961        };
962
963        assert_eq!(event.response_text, "Done!");
964        assert_eq!(event.tool_calls_count, 3);
965        assert_eq!(event.usage.total_tokens, 150);
966
967        let hook_event = HookEvent::PostResponse(event);
968        assert_eq!(hook_event.event_type(), HookEventType::PostResponse);
969        assert_eq!(hook_event.session_id(), "s1");
970    }
971
972    #[test]
973    fn test_on_error_event() {
974        let event = OnErrorEvent {
975            session_id: "s1".to_string(),
976            error_type: ErrorType::ToolFailure,
977            error_message: "Command failed with exit code 1".to_string(),
978            context: serde_json::json!({"tool": "Bash", "command": "false"}),
979        };
980
981        assert_eq!(event.error_type.to_string(), "tool_failure");
982        assert_eq!(event.error_message, "Command failed with exit code 1");
983
984        let hook_event = HookEvent::OnError(event);
985        assert_eq!(hook_event.event_type(), HookEventType::OnError);
986        assert_eq!(hook_event.session_id(), "s1");
987    }
988
989    #[test]
990    fn test_error_type_display() {
991        assert_eq!(ErrorType::ToolFailure.to_string(), "tool_failure");
992        assert_eq!(ErrorType::LlmFailure.to_string(), "llm_failure");
993        assert_eq!(ErrorType::PermissionDenied.to_string(), "permission_denied");
994        assert_eq!(ErrorType::Timeout.to_string(), "timeout");
995        assert_eq!(ErrorType::Other.to_string(), "other");
996    }
997
998    #[test]
999    fn test_new_event_serialization() {
1000        // PrePrompt
1001        let event = HookEvent::PrePrompt(PrePromptEvent {
1002            session_id: "s1".to_string(),
1003            prompt: "Hello".to_string(),
1004            system_prompt: None,
1005            message_count: 0,
1006        });
1007        let json = serde_json::to_string(&event).unwrap();
1008        assert!(json.contains("pre_prompt"));
1009        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
1010        assert_eq!(parsed.event_type(), HookEventType::PrePrompt);
1011
1012        // PostResponse
1013        let event = HookEvent::PostResponse(PostResponseEvent {
1014            session_id: "s1".to_string(),
1015            response_text: "Hi".to_string(),
1016            tool_calls_count: 0,
1017            usage: TokenUsageInfo {
1018                prompt_tokens: 10,
1019                completion_tokens: 5,
1020                total_tokens: 15,
1021            },
1022            duration_ms: 100,
1023        });
1024        let json = serde_json::to_string(&event).unwrap();
1025        assert!(json.contains("post_response"));
1026        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
1027        assert_eq!(parsed.event_type(), HookEventType::PostResponse);
1028
1029        // OnError
1030        let event = HookEvent::OnError(OnErrorEvent {
1031            session_id: "s1".to_string(),
1032            error_type: ErrorType::LlmFailure,
1033            error_message: "API timeout".to_string(),
1034            context: serde_json::json!({}),
1035        });
1036        let json = serde_json::to_string(&event).unwrap();
1037        assert!(json.contains("on_error"));
1038        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
1039        assert_eq!(parsed.event_type(), HookEventType::OnError);
1040    }
1041}