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 PrePrompt,
29 PostResponse,
31 OnError,
33}
34
35impl std::fmt::Display for HookEventType {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 HookEventType::PreToolUse => write!(f, "pre_tool_use"),
39 HookEventType::PostToolUse => write!(f, "post_tool_use"),
40 HookEventType::GenerateStart => write!(f, "generate_start"),
41 HookEventType::GenerateEnd => write!(f, "generate_end"),
42 HookEventType::SessionStart => write!(f, "session_start"),
43 HookEventType::SessionEnd => write!(f, "session_end"),
44 HookEventType::SkillLoad => write!(f, "skill_load"),
45 HookEventType::SkillUnload => write!(f, "skill_unload"),
46 HookEventType::PrePrompt => write!(f, "pre_prompt"),
47 HookEventType::PostResponse => write!(f, "post_response"),
48 HookEventType::OnError => write!(f, "on_error"),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ToolResultData {
56 pub success: bool,
58 pub output: String,
60 pub exit_code: Option<i32>,
62 pub duration_ms: u64,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct PreToolUseEvent {
69 pub session_id: String,
71 pub tool: String,
73 pub args: serde_json::Value,
75 pub working_directory: String,
77 pub recent_tools: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct PostToolUseEvent {
84 pub session_id: String,
86 pub tool: String,
88 pub args: serde_json::Value,
90 pub result: ToolResultData,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct GenerateStartEvent {
97 pub session_id: String,
99 pub prompt: String,
101 pub system_prompt: Option<String>,
103 pub model_provider: String,
105 pub model_name: String,
107 pub available_tools: Vec<String>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct GenerateEndEvent {
114 pub session_id: String,
116 pub prompt: String,
118 pub response_text: String,
120 pub tool_calls: Vec<ToolCallInfo>,
122 pub usage: TokenUsageInfo,
124 pub duration_ms: u64,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ToolCallInfo {
131 pub name: String,
133 pub args: serde_json::Value,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct TokenUsageInfo {
140 pub prompt_tokens: i32,
142 pub completion_tokens: i32,
144 pub total_tokens: i32,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SessionStartEvent {
151 pub session_id: String,
153 pub system_prompt: Option<String>,
155 pub model_provider: String,
157 pub model_name: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct SessionEndEvent {
163 pub session_id: String,
165 pub total_tokens: i32,
167 pub total_tool_calls: i32,
169 pub duration_ms: u64,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SkillLoadEvent {
176 pub skill_name: String,
178 pub tool_names: Vec<String>,
180 pub version: Option<String>,
182 pub description: Option<String>,
184 pub loaded_at: i64,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct SkillUnloadEvent {
191 pub skill_name: String,
193 pub tool_names: Vec<String>,
195 pub duration_ms: u64,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PrePromptEvent {
202 pub session_id: String,
204 pub prompt: String,
206 pub system_prompt: Option<String>,
208 pub message_count: usize,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct PostResponseEvent {
215 pub session_id: String,
217 pub response_text: String,
219 pub tool_calls_count: usize,
221 pub usage: TokenUsageInfo,
223 pub duration_ms: u64,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum ErrorType {
231 ToolFailure,
233 LlmFailure,
235 PermissionDenied,
237 Timeout,
239 Other,
241}
242
243impl std::fmt::Display for ErrorType {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 match self {
246 ErrorType::ToolFailure => write!(f, "tool_failure"),
247 ErrorType::LlmFailure => write!(f, "llm_failure"),
248 ErrorType::PermissionDenied => write!(f, "permission_denied"),
249 ErrorType::Timeout => write!(f, "timeout"),
250 ErrorType::Other => write!(f, "other"),
251 }
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct OnErrorEvent {
258 pub session_id: String,
260 pub error_type: ErrorType,
262 pub error_message: String,
264 pub context: serde_json::Value,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(tag = "event_type", content = "payload")]
271pub enum HookEvent {
272 #[serde(rename = "pre_tool_use")]
273 PreToolUse(PreToolUseEvent),
274 #[serde(rename = "post_tool_use")]
275 PostToolUse(PostToolUseEvent),
276 #[serde(rename = "generate_start")]
277 GenerateStart(GenerateStartEvent),
278 #[serde(rename = "generate_end")]
279 GenerateEnd(GenerateEndEvent),
280 #[serde(rename = "session_start")]
281 SessionStart(SessionStartEvent),
282 #[serde(rename = "session_end")]
283 SessionEnd(SessionEndEvent),
284 #[serde(rename = "skill_load")]
285 SkillLoad(SkillLoadEvent),
286 #[serde(rename = "skill_unload")]
287 SkillUnload(SkillUnloadEvent),
288 #[serde(rename = "pre_prompt")]
289 PrePrompt(PrePromptEvent),
290 #[serde(rename = "post_response")]
291 PostResponse(PostResponseEvent),
292 #[serde(rename = "on_error")]
293 OnError(OnErrorEvent),
294}
295
296impl HookEvent {
297 pub fn event_type(&self) -> HookEventType {
299 match self {
300 HookEvent::PreToolUse(_) => HookEventType::PreToolUse,
301 HookEvent::PostToolUse(_) => HookEventType::PostToolUse,
302 HookEvent::GenerateStart(_) => HookEventType::GenerateStart,
303 HookEvent::GenerateEnd(_) => HookEventType::GenerateEnd,
304 HookEvent::SessionStart(_) => HookEventType::SessionStart,
305 HookEvent::SessionEnd(_) => HookEventType::SessionEnd,
306 HookEvent::SkillLoad(_) => HookEventType::SkillLoad,
307 HookEvent::SkillUnload(_) => HookEventType::SkillUnload,
308 HookEvent::PrePrompt(_) => HookEventType::PrePrompt,
309 HookEvent::PostResponse(_) => HookEventType::PostResponse,
310 HookEvent::OnError(_) => HookEventType::OnError,
311 }
312 }
313
314 pub fn session_id(&self) -> &str {
316 match self {
317 HookEvent::PreToolUse(e) => &e.session_id,
318 HookEvent::PostToolUse(e) => &e.session_id,
319 HookEvent::GenerateStart(e) => &e.session_id,
320 HookEvent::GenerateEnd(e) => &e.session_id,
321 HookEvent::SessionStart(e) => &e.session_id,
322 HookEvent::SessionEnd(e) => &e.session_id,
323 HookEvent::PrePrompt(e) => &e.session_id,
324 HookEvent::PostResponse(e) => &e.session_id,
325 HookEvent::OnError(e) => &e.session_id,
326 HookEvent::SkillLoad(_) => "",
328 HookEvent::SkillUnload(_) => "",
329 }
330 }
331
332 pub fn tool_name(&self) -> Option<&str> {
334 match self {
335 HookEvent::PreToolUse(e) => Some(&e.tool),
336 HookEvent::PostToolUse(e) => Some(&e.tool),
337 _ => None,
338 }
339 }
340
341 pub fn tool_args(&self) -> Option<&serde_json::Value> {
343 match self {
344 HookEvent::PreToolUse(e) => Some(&e.args),
345 HookEvent::PostToolUse(e) => Some(&e.args),
346 _ => None,
347 }
348 }
349
350 pub fn skill_name(&self) -> Option<&str> {
352 match self {
353 HookEvent::SkillLoad(e) => Some(&e.skill_name),
354 HookEvent::SkillUnload(e) => Some(&e.skill_name),
355 _ => None,
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_hook_event_type_display() {
366 assert_eq!(HookEventType::PreToolUse.to_string(), "pre_tool_use");
367 assert_eq!(HookEventType::PostToolUse.to_string(), "post_tool_use");
368 assert_eq!(HookEventType::GenerateStart.to_string(), "generate_start");
369 assert_eq!(HookEventType::GenerateEnd.to_string(), "generate_end");
370 assert_eq!(HookEventType::SessionStart.to_string(), "session_start");
371 assert_eq!(HookEventType::SessionEnd.to_string(), "session_end");
372 assert_eq!(HookEventType::SkillLoad.to_string(), "skill_load");
373 assert_eq!(HookEventType::SkillUnload.to_string(), "skill_unload");
374 }
375
376 #[test]
377 fn test_pre_tool_use_event() {
378 let event = PreToolUseEvent {
379 session_id: "session-1".to_string(),
380 tool: "Bash".to_string(),
381 args: serde_json::json!({"command": "echo hello"}),
382 working_directory: "/workspace".to_string(),
383 recent_tools: vec!["Read".to_string()],
384 };
385
386 assert_eq!(event.session_id, "session-1");
387 assert_eq!(event.tool, "Bash");
388 }
389
390 #[test]
391 fn test_post_tool_use_event() {
392 let event = PostToolUseEvent {
393 session_id: "session-1".to_string(),
394 tool: "Bash".to_string(),
395 args: serde_json::json!({"command": "echo hello"}),
396 result: ToolResultData {
397 success: true,
398 output: "hello\n".to_string(),
399 exit_code: Some(0),
400 duration_ms: 50,
401 },
402 };
403
404 assert!(event.result.success);
405 assert_eq!(event.result.exit_code, Some(0));
406 }
407
408 #[test]
409 fn test_hook_event_type() {
410 let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
411 session_id: "s1".to_string(),
412 tool: "Bash".to_string(),
413 args: serde_json::json!({}),
414 working_directory: "/".to_string(),
415 recent_tools: vec![],
416 });
417
418 assert_eq!(pre_tool.event_type(), HookEventType::PreToolUse);
419 assert_eq!(pre_tool.session_id(), "s1");
420 assert_eq!(pre_tool.tool_name(), Some("Bash"));
421 }
422
423 #[test]
424 fn test_hook_event_serialization() {
425 let event = HookEvent::PreToolUse(PreToolUseEvent {
426 session_id: "s1".to_string(),
427 tool: "Bash".to_string(),
428 args: serde_json::json!({"command": "ls"}),
429 working_directory: "/workspace".to_string(),
430 recent_tools: vec![],
431 });
432
433 let json = serde_json::to_string(&event).unwrap();
434 assert!(json.contains("pre_tool_use"));
435 assert!(json.contains("Bash"));
436
437 let parsed: HookEvent = serde_json::from_str(&json).unwrap();
439 assert_eq!(parsed.event_type(), HookEventType::PreToolUse);
440 }
441
442 #[test]
443 fn test_generate_events() {
444 let start = GenerateStartEvent {
445 session_id: "s1".to_string(),
446 prompt: "Hello".to_string(),
447 system_prompt: Some("You are helpful".to_string()),
448 model_provider: "anthropic".to_string(),
449 model_name: "claude-3".to_string(),
450 available_tools: vec!["Bash".to_string(), "Read".to_string()],
451 };
452
453 let end = GenerateEndEvent {
454 session_id: "s1".to_string(),
455 prompt: "Hello".to_string(),
456 response_text: "Hi there!".to_string(),
457 tool_calls: vec![],
458 usage: TokenUsageInfo {
459 prompt_tokens: 10,
460 completion_tokens: 5,
461 total_tokens: 15,
462 },
463 duration_ms: 500,
464 };
465
466 assert_eq!(start.prompt, "Hello");
467 assert_eq!(end.response_text, "Hi there!");
468 assert_eq!(end.usage.total_tokens, 15);
469 }
470
471 #[test]
472 fn test_session_events() {
473 let start = SessionStartEvent {
474 session_id: "s1".to_string(),
475 system_prompt: Some("System".to_string()),
476 model_provider: "anthropic".to_string(),
477 model_name: "claude-3".to_string(),
478 };
479
480 let end = SessionEndEvent {
481 session_id: "s1".to_string(),
482 total_tokens: 1000,
483 total_tool_calls: 5,
484 duration_ms: 60000,
485 };
486
487 let start_event = HookEvent::SessionStart(start);
488 let end_event = HookEvent::SessionEnd(end);
489
490 assert_eq!(start_event.event_type(), HookEventType::SessionStart);
491 assert_eq!(end_event.event_type(), HookEventType::SessionEnd);
492 assert!(start_event.tool_name().is_none());
493 }
494
495 #[test]
496 fn test_skill_load_event() {
497 let event = SkillLoadEvent {
498 skill_name: "test-skill".to_string(),
499 tool_names: vec!["tool1".to_string(), "tool2".to_string()],
500 version: Some("1.0.0".to_string()),
501 description: Some("A test skill".to_string()),
502 loaded_at: 1234567890,
503 };
504
505 assert_eq!(event.skill_name, "test-skill");
506 assert_eq!(event.tool_names.len(), 2);
507 assert_eq!(event.version, Some("1.0.0".to_string()));
508 assert_eq!(event.loaded_at, 1234567890);
509 }
510
511 #[test]
512 fn test_skill_unload_event() {
513 let event = SkillUnloadEvent {
514 skill_name: "test-skill".to_string(),
515 tool_names: vec!["tool1".to_string(), "tool2".to_string()],
516 duration_ms: 60000,
517 };
518
519 assert_eq!(event.skill_name, "test-skill");
520 assert_eq!(event.tool_names.len(), 2);
521 assert_eq!(event.duration_ms, 60000);
522 }
523
524 #[test]
525 fn test_hook_event_skill_name() {
526 let load_event = HookEvent::SkillLoad(SkillLoadEvent {
527 skill_name: "my-skill".to_string(),
528 tool_names: vec!["tool1".to_string()],
529 version: None,
530 description: None,
531 loaded_at: 0,
532 });
533
534 let unload_event = HookEvent::SkillUnload(SkillUnloadEvent {
535 skill_name: "my-skill".to_string(),
536 tool_names: vec!["tool1".to_string()],
537 duration_ms: 1000,
538 });
539
540 assert_eq!(load_event.event_type(), HookEventType::SkillLoad);
541 assert_eq!(load_event.skill_name(), Some("my-skill"));
542 assert_eq!(load_event.session_id(), ""); assert_eq!(unload_event.event_type(), HookEventType::SkillUnload);
545 assert_eq!(unload_event.skill_name(), Some("my-skill"));
546 assert_eq!(unload_event.session_id(), ""); let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
550 session_id: "s1".to_string(),
551 tool: "Bash".to_string(),
552 args: serde_json::json!({}),
553 working_directory: "/".to_string(),
554 recent_tools: vec![],
555 });
556 assert!(pre_tool.skill_name().is_none());
557 }
558
559 #[test]
560 fn test_skill_event_serialization() {
561 let event = HookEvent::SkillLoad(SkillLoadEvent {
562 skill_name: "test-skill".to_string(),
563 tool_names: vec!["tool1".to_string()],
564 version: Some("1.0.0".to_string()),
565 description: None,
566 loaded_at: 1234567890,
567 });
568
569 let json = serde_json::to_string(&event).unwrap();
570 assert!(json.contains("skill_load"));
571 assert!(json.contains("test-skill"));
572 assert!(json.contains("1.0.0"));
573
574 let parsed: HookEvent = serde_json::from_str(&json).unwrap();
575 assert_eq!(parsed.event_type(), HookEventType::SkillLoad);
576 assert_eq!(parsed.skill_name(), Some("test-skill"));
577 }
578
579 #[test]
580 fn test_hook_event_type_display_new_variants() {
581 assert_eq!(HookEventType::PrePrompt.to_string(), "pre_prompt");
582 assert_eq!(HookEventType::PostResponse.to_string(), "post_response");
583 assert_eq!(HookEventType::OnError.to_string(), "on_error");
584 }
585
586 #[test]
587 fn test_pre_prompt_event() {
588 let event = PrePromptEvent {
589 session_id: "s1".to_string(),
590 prompt: "Fix the bug".to_string(),
591 system_prompt: Some("You are helpful".to_string()),
592 message_count: 5,
593 };
594
595 assert_eq!(event.session_id, "s1");
596 assert_eq!(event.prompt, "Fix the bug");
597 assert_eq!(event.message_count, 5);
598
599 let hook_event = HookEvent::PrePrompt(event);
600 assert_eq!(hook_event.event_type(), HookEventType::PrePrompt);
601 assert_eq!(hook_event.session_id(), "s1");
602 assert!(hook_event.tool_name().is_none());
603 assert!(hook_event.skill_name().is_none());
604 }
605
606 #[test]
607 fn test_post_response_event() {
608 let event = PostResponseEvent {
609 session_id: "s1".to_string(),
610 response_text: "Done!".to_string(),
611 tool_calls_count: 3,
612 usage: TokenUsageInfo {
613 prompt_tokens: 100,
614 completion_tokens: 50,
615 total_tokens: 150,
616 },
617 duration_ms: 2000,
618 };
619
620 assert_eq!(event.response_text, "Done!");
621 assert_eq!(event.tool_calls_count, 3);
622 assert_eq!(event.usage.total_tokens, 150);
623
624 let hook_event = HookEvent::PostResponse(event);
625 assert_eq!(hook_event.event_type(), HookEventType::PostResponse);
626 assert_eq!(hook_event.session_id(), "s1");
627 }
628
629 #[test]
630 fn test_on_error_event() {
631 let event = OnErrorEvent {
632 session_id: "s1".to_string(),
633 error_type: ErrorType::ToolFailure,
634 error_message: "Command failed with exit code 1".to_string(),
635 context: serde_json::json!({"tool": "Bash", "command": "false"}),
636 };
637
638 assert_eq!(event.error_type.to_string(), "tool_failure");
639 assert_eq!(event.error_message, "Command failed with exit code 1");
640
641 let hook_event = HookEvent::OnError(event);
642 assert_eq!(hook_event.event_type(), HookEventType::OnError);
643 assert_eq!(hook_event.session_id(), "s1");
644 }
645
646 #[test]
647 fn test_error_type_display() {
648 assert_eq!(ErrorType::ToolFailure.to_string(), "tool_failure");
649 assert_eq!(ErrorType::LlmFailure.to_string(), "llm_failure");
650 assert_eq!(ErrorType::PermissionDenied.to_string(), "permission_denied");
651 assert_eq!(ErrorType::Timeout.to_string(), "timeout");
652 assert_eq!(ErrorType::Other.to_string(), "other");
653 }
654
655 #[test]
656 fn test_new_event_serialization() {
657 let event = HookEvent::PrePrompt(PrePromptEvent {
659 session_id: "s1".to_string(),
660 prompt: "Hello".to_string(),
661 system_prompt: None,
662 message_count: 0,
663 });
664 let json = serde_json::to_string(&event).unwrap();
665 assert!(json.contains("pre_prompt"));
666 let parsed: HookEvent = serde_json::from_str(&json).unwrap();
667 assert_eq!(parsed.event_type(), HookEventType::PrePrompt);
668
669 let event = HookEvent::PostResponse(PostResponseEvent {
671 session_id: "s1".to_string(),
672 response_text: "Hi".to_string(),
673 tool_calls_count: 0,
674 usage: TokenUsageInfo {
675 prompt_tokens: 10,
676 completion_tokens: 5,
677 total_tokens: 15,
678 },
679 duration_ms: 100,
680 });
681 let json = serde_json::to_string(&event).unwrap();
682 assert!(json.contains("post_response"));
683 let parsed: HookEvent = serde_json::from_str(&json).unwrap();
684 assert_eq!(parsed.event_type(), HookEventType::PostResponse);
685
686 let event = HookEvent::OnError(OnErrorEvent {
688 session_id: "s1".to_string(),
689 error_type: ErrorType::LlmFailure,
690 error_message: "API timeout".to_string(),
691 context: serde_json::json!({}),
692 });
693 let json = serde_json::to_string(&event).unwrap();
694 assert!(json.contains("on_error"));
695 let parsed: HookEvent = serde_json::from_str(&json).unwrap();
696 assert_eq!(parsed.event_type(), HookEventType::OnError);
697 }
698}