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