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}
28
29impl std::fmt::Display for HookEventType {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            HookEventType::PreToolUse => write!(f, "pre_tool_use"),
33            HookEventType::PostToolUse => write!(f, "post_tool_use"),
34            HookEventType::GenerateStart => write!(f, "generate_start"),
35            HookEventType::GenerateEnd => write!(f, "generate_end"),
36            HookEventType::SessionStart => write!(f, "session_start"),
37            HookEventType::SessionEnd => write!(f, "session_end"),
38            HookEventType::SkillLoad => write!(f, "skill_load"),
39            HookEventType::SkillUnload => write!(f, "skill_unload"),
40        }
41    }
42}
43
44/// Tool execution result data
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ToolResultData {
47    /// Whether execution succeeded
48    pub success: bool,
49    /// Tool output
50    pub output: String,
51    /// Exit code (for shell commands)
52    pub exit_code: Option<i32>,
53    /// Execution duration in milliseconds
54    pub duration_ms: u64,
55}
56
57/// Pre-tool-use event payload
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct PreToolUseEvent {
60    /// Session ID
61    pub session_id: String,
62    /// Tool name
63    pub tool: String,
64    /// Tool arguments
65    pub args: serde_json::Value,
66    /// Working directory
67    pub working_directory: String,
68    /// Recent tools executed (for context)
69    pub recent_tools: Vec<String>,
70}
71
72/// Post-tool-use event payload
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct PostToolUseEvent {
75    /// Session ID
76    pub session_id: String,
77    /// Tool name
78    pub tool: String,
79    /// Tool arguments
80    pub args: serde_json::Value,
81    /// Execution result
82    pub result: ToolResultData,
83}
84
85/// Generate start event payload
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct GenerateStartEvent {
88    /// Session ID
89    pub session_id: String,
90    /// User prompt
91    pub prompt: String,
92    /// System prompt (if any)
93    pub system_prompt: Option<String>,
94    /// Model provider
95    pub model_provider: String,
96    /// Model name
97    pub model_name: String,
98    /// Available tools
99    pub available_tools: Vec<String>,
100}
101
102/// Generate end event payload
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct GenerateEndEvent {
105    /// Session ID
106    pub session_id: String,
107    /// User prompt
108    pub prompt: String,
109    /// Response text
110    pub response_text: String,
111    /// Tool calls made
112    pub tool_calls: Vec<ToolCallInfo>,
113    /// Token usage
114    pub usage: TokenUsageInfo,
115    /// Duration in milliseconds
116    pub duration_ms: u64,
117}
118
119/// Tool call information
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ToolCallInfo {
122    /// Tool name
123    pub name: String,
124    /// Tool arguments
125    pub args: serde_json::Value,
126}
127
128/// Token usage information
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct TokenUsageInfo {
131    /// Prompt tokens
132    pub prompt_tokens: i32,
133    /// Completion tokens
134    pub completion_tokens: i32,
135    /// Total tokens
136    pub total_tokens: i32,
137}
138
139/// Session start event payload
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SessionStartEvent {
142    /// Session ID
143    pub session_id: String,
144    /// System prompt (if any)
145    pub system_prompt: Option<String>,
146    /// Model configuration
147    pub model_provider: String,
148    pub model_name: String,
149}
150
151/// Session end event payload
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct SessionEndEvent {
154    /// Session ID
155    pub session_id: String,
156    /// Total token usage
157    pub total_tokens: i32,
158    /// Total tool calls
159    pub total_tool_calls: i32,
160    /// Session duration in milliseconds
161    pub duration_ms: u64,
162}
163
164/// Skill load event payload
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct SkillLoadEvent {
167    /// Skill name
168    pub skill_name: String,
169    /// Tool names loaded from the skill
170    pub tool_names: Vec<String>,
171    /// Skill version (if available)
172    pub version: Option<String>,
173    /// Skill description (if available)
174    pub description: Option<String>,
175    /// Timestamp when skill was loaded (Unix milliseconds)
176    pub loaded_at: i64,
177}
178
179/// Skill unload event payload
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct SkillUnloadEvent {
182    /// Skill name
183    pub skill_name: String,
184    /// Tool names that were unloaded
185    pub tool_names: Vec<String>,
186    /// How long the skill was loaded (milliseconds)
187    pub duration_ms: u64,
188}
189
190/// Unified hook event enum
191#[derive(Debug, Clone, Serialize, Deserialize)]
192#[serde(tag = "event_type", content = "payload")]
193pub enum HookEvent {
194    #[serde(rename = "pre_tool_use")]
195    PreToolUse(PreToolUseEvent),
196    #[serde(rename = "post_tool_use")]
197    PostToolUse(PostToolUseEvent),
198    #[serde(rename = "generate_start")]
199    GenerateStart(GenerateStartEvent),
200    #[serde(rename = "generate_end")]
201    GenerateEnd(GenerateEndEvent),
202    #[serde(rename = "session_start")]
203    SessionStart(SessionStartEvent),
204    #[serde(rename = "session_end")]
205    SessionEnd(SessionEndEvent),
206    #[serde(rename = "skill_load")]
207    SkillLoad(SkillLoadEvent),
208    #[serde(rename = "skill_unload")]
209    SkillUnload(SkillUnloadEvent),
210}
211
212impl HookEvent {
213    /// Get the event type
214    pub fn event_type(&self) -> HookEventType {
215        match self {
216            HookEvent::PreToolUse(_) => HookEventType::PreToolUse,
217            HookEvent::PostToolUse(_) => HookEventType::PostToolUse,
218            HookEvent::GenerateStart(_) => HookEventType::GenerateStart,
219            HookEvent::GenerateEnd(_) => HookEventType::GenerateEnd,
220            HookEvent::SessionStart(_) => HookEventType::SessionStart,
221            HookEvent::SessionEnd(_) => HookEventType::SessionEnd,
222            HookEvent::SkillLoad(_) => HookEventType::SkillLoad,
223            HookEvent::SkillUnload(_) => HookEventType::SkillUnload,
224        }
225    }
226
227    /// Get the session ID (returns empty string for skill events which are global)
228    pub fn session_id(&self) -> &str {
229        match self {
230            HookEvent::PreToolUse(e) => &e.session_id,
231            HookEvent::PostToolUse(e) => &e.session_id,
232            HookEvent::GenerateStart(e) => &e.session_id,
233            HookEvent::GenerateEnd(e) => &e.session_id,
234            HookEvent::SessionStart(e) => &e.session_id,
235            HookEvent::SessionEnd(e) => &e.session_id,
236            // Skill events are global (not session-specific)
237            HookEvent::SkillLoad(_) => "",
238            HookEvent::SkillUnload(_) => "",
239        }
240    }
241
242    /// Get the tool name (for tool events)
243    pub fn tool_name(&self) -> Option<&str> {
244        match self {
245            HookEvent::PreToolUse(e) => Some(&e.tool),
246            HookEvent::PostToolUse(e) => Some(&e.tool),
247            _ => None,
248        }
249    }
250
251    /// Get the tool args (for tool events)
252    pub fn tool_args(&self) -> Option<&serde_json::Value> {
253        match self {
254            HookEvent::PreToolUse(e) => Some(&e.args),
255            HookEvent::PostToolUse(e) => Some(&e.args),
256            _ => None,
257        }
258    }
259
260    /// Get the skill name (for skill events)
261    pub fn skill_name(&self) -> Option<&str> {
262        match self {
263            HookEvent::SkillLoad(e) => Some(&e.skill_name),
264            HookEvent::SkillUnload(e) => Some(&e.skill_name),
265            _ => None,
266        }
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_hook_event_type_display() {
276        assert_eq!(HookEventType::PreToolUse.to_string(), "pre_tool_use");
277        assert_eq!(HookEventType::PostToolUse.to_string(), "post_tool_use");
278        assert_eq!(HookEventType::GenerateStart.to_string(), "generate_start");
279        assert_eq!(HookEventType::GenerateEnd.to_string(), "generate_end");
280        assert_eq!(HookEventType::SessionStart.to_string(), "session_start");
281        assert_eq!(HookEventType::SessionEnd.to_string(), "session_end");
282        assert_eq!(HookEventType::SkillLoad.to_string(), "skill_load");
283        assert_eq!(HookEventType::SkillUnload.to_string(), "skill_unload");
284    }
285
286    #[test]
287    fn test_pre_tool_use_event() {
288        let event = PreToolUseEvent {
289            session_id: "session-1".to_string(),
290            tool: "Bash".to_string(),
291            args: serde_json::json!({"command": "echo hello"}),
292            working_directory: "/workspace".to_string(),
293            recent_tools: vec!["Read".to_string()],
294        };
295
296        assert_eq!(event.session_id, "session-1");
297        assert_eq!(event.tool, "Bash");
298    }
299
300    #[test]
301    fn test_post_tool_use_event() {
302        let event = PostToolUseEvent {
303            session_id: "session-1".to_string(),
304            tool: "Bash".to_string(),
305            args: serde_json::json!({"command": "echo hello"}),
306            result: ToolResultData {
307                success: true,
308                output: "hello\n".to_string(),
309                exit_code: Some(0),
310                duration_ms: 50,
311            },
312        };
313
314        assert!(event.result.success);
315        assert_eq!(event.result.exit_code, Some(0));
316    }
317
318    #[test]
319    fn test_hook_event_type() {
320        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
321            session_id: "s1".to_string(),
322            tool: "Bash".to_string(),
323            args: serde_json::json!({}),
324            working_directory: "/".to_string(),
325            recent_tools: vec![],
326        });
327
328        assert_eq!(pre_tool.event_type(), HookEventType::PreToolUse);
329        assert_eq!(pre_tool.session_id(), "s1");
330        assert_eq!(pre_tool.tool_name(), Some("Bash"));
331    }
332
333    #[test]
334    fn test_hook_event_serialization() {
335        let event = HookEvent::PreToolUse(PreToolUseEvent {
336            session_id: "s1".to_string(),
337            tool: "Bash".to_string(),
338            args: serde_json::json!({"command": "ls"}),
339            working_directory: "/workspace".to_string(),
340            recent_tools: vec![],
341        });
342
343        let json = serde_json::to_string(&event).unwrap();
344        assert!(json.contains("pre_tool_use"));
345        assert!(json.contains("Bash"));
346
347        // Deserialize back
348        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
349        assert_eq!(parsed.event_type(), HookEventType::PreToolUse);
350    }
351
352    #[test]
353    fn test_generate_events() {
354        let start = GenerateStartEvent {
355            session_id: "s1".to_string(),
356            prompt: "Hello".to_string(),
357            system_prompt: Some("You are helpful".to_string()),
358            model_provider: "anthropic".to_string(),
359            model_name: "claude-3".to_string(),
360            available_tools: vec!["Bash".to_string(), "Read".to_string()],
361        };
362
363        let end = GenerateEndEvent {
364            session_id: "s1".to_string(),
365            prompt: "Hello".to_string(),
366            response_text: "Hi there!".to_string(),
367            tool_calls: vec![],
368            usage: TokenUsageInfo {
369                prompt_tokens: 10,
370                completion_tokens: 5,
371                total_tokens: 15,
372            },
373            duration_ms: 500,
374        };
375
376        assert_eq!(start.prompt, "Hello");
377        assert_eq!(end.response_text, "Hi there!");
378        assert_eq!(end.usage.total_tokens, 15);
379    }
380
381    #[test]
382    fn test_session_events() {
383        let start = SessionStartEvent {
384            session_id: "s1".to_string(),
385            system_prompt: Some("System".to_string()),
386            model_provider: "anthropic".to_string(),
387            model_name: "claude-3".to_string(),
388        };
389
390        let end = SessionEndEvent {
391            session_id: "s1".to_string(),
392            total_tokens: 1000,
393            total_tool_calls: 5,
394            duration_ms: 60000,
395        };
396
397        let start_event = HookEvent::SessionStart(start);
398        let end_event = HookEvent::SessionEnd(end);
399
400        assert_eq!(start_event.event_type(), HookEventType::SessionStart);
401        assert_eq!(end_event.event_type(), HookEventType::SessionEnd);
402        assert!(start_event.tool_name().is_none());
403    }
404
405    #[test]
406    fn test_skill_load_event() {
407        let event = SkillLoadEvent {
408            skill_name: "test-skill".to_string(),
409            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
410            version: Some("1.0.0".to_string()),
411            description: Some("A test skill".to_string()),
412            loaded_at: 1234567890,
413        };
414
415        assert_eq!(event.skill_name, "test-skill");
416        assert_eq!(event.tool_names.len(), 2);
417        assert_eq!(event.version, Some("1.0.0".to_string()));
418        assert_eq!(event.loaded_at, 1234567890);
419    }
420
421    #[test]
422    fn test_skill_unload_event() {
423        let event = SkillUnloadEvent {
424            skill_name: "test-skill".to_string(),
425            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
426            duration_ms: 60000,
427        };
428
429        assert_eq!(event.skill_name, "test-skill");
430        assert_eq!(event.tool_names.len(), 2);
431        assert_eq!(event.duration_ms, 60000);
432    }
433
434    #[test]
435    fn test_hook_event_skill_name() {
436        let load_event = HookEvent::SkillLoad(SkillLoadEvent {
437            skill_name: "my-skill".to_string(),
438            tool_names: vec!["tool1".to_string()],
439            version: None,
440            description: None,
441            loaded_at: 0,
442        });
443
444        let unload_event = HookEvent::SkillUnload(SkillUnloadEvent {
445            skill_name: "my-skill".to_string(),
446            tool_names: vec!["tool1".to_string()],
447            duration_ms: 1000,
448        });
449
450        assert_eq!(load_event.event_type(), HookEventType::SkillLoad);
451        assert_eq!(load_event.skill_name(), Some("my-skill"));
452        assert_eq!(load_event.session_id(), ""); // Skills are global
453
454        assert_eq!(unload_event.event_type(), HookEventType::SkillUnload);
455        assert_eq!(unload_event.skill_name(), Some("my-skill"));
456        assert_eq!(unload_event.session_id(), ""); // Skills are global
457
458        // Non-skill events return None for skill_name
459        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
460            session_id: "s1".to_string(),
461            tool: "Bash".to_string(),
462            args: serde_json::json!({}),
463            working_directory: "/".to_string(),
464            recent_tools: vec![],
465        });
466        assert!(pre_tool.skill_name().is_none());
467    }
468
469    #[test]
470    fn test_skill_event_serialization() {
471        let event = HookEvent::SkillLoad(SkillLoadEvent {
472            skill_name: "test-skill".to_string(),
473            tool_names: vec!["tool1".to_string()],
474            version: Some("1.0.0".to_string()),
475            description: None,
476            loaded_at: 1234567890,
477        });
478
479        let json = serde_json::to_string(&event).unwrap();
480        assert!(json.contains("skill_load"));
481        assert!(json.contains("test-skill"));
482        assert!(json.contains("1.0.0"));
483
484        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
485        assert_eq!(parsed.event_type(), HookEventType::SkillLoad);
486        assert_eq!(parsed.skill_name(), Some("test-skill"));
487    }
488}