1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum HookEventType {
11 PreToolUse,
13 PostToolUse,
15 GenerateStart,
17 GenerateEnd,
19 SessionStart,
21 SessionEnd,
23 SkillLoad,
25 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#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ToolResultData {
47 pub success: bool,
49 pub output: String,
51 pub exit_code: Option<i32>,
53 pub duration_ms: u64,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct PreToolUseEvent {
60 pub session_id: String,
62 pub tool: String,
64 pub args: serde_json::Value,
66 pub working_directory: String,
68 pub recent_tools: Vec<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct PostToolUseEvent {
75 pub session_id: String,
77 pub tool: String,
79 pub args: serde_json::Value,
81 pub result: ToolResultData,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct GenerateStartEvent {
88 pub session_id: String,
90 pub prompt: String,
92 pub system_prompt: Option<String>,
94 pub model_provider: String,
96 pub model_name: String,
98 pub available_tools: Vec<String>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct GenerateEndEvent {
105 pub session_id: String,
107 pub prompt: String,
109 pub response_text: String,
111 pub tool_calls: Vec<ToolCallInfo>,
113 pub usage: TokenUsageInfo,
115 pub duration_ms: u64,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ToolCallInfo {
122 pub name: String,
124 pub args: serde_json::Value,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct TokenUsageInfo {
131 pub prompt_tokens: i32,
133 pub completion_tokens: i32,
135 pub total_tokens: i32,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SessionStartEvent {
142 pub session_id: String,
144 pub system_prompt: Option<String>,
146 pub model_provider: String,
148 pub model_name: String,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct SessionEndEvent {
154 pub session_id: String,
156 pub total_tokens: i32,
158 pub total_tool_calls: i32,
160 pub duration_ms: u64,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct SkillLoadEvent {
167 pub skill_name: String,
169 pub tool_names: Vec<String>,
171 pub version: Option<String>,
173 pub description: Option<String>,
175 pub loaded_at: i64,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct SkillUnloadEvent {
182 pub skill_name: String,
184 pub tool_names: Vec<String>,
186 pub duration_ms: u64,
188}
189
190#[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 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 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 HookEvent::SkillLoad(_) => "",
238 HookEvent::SkillUnload(_) => "",
239 }
240 }
241
242 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 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 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 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(), ""); 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(), ""); 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}