use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HookEventType {
PreToolUse,
PostToolUse,
GenerateStart,
GenerateEnd,
SessionStart,
SessionEnd,
SkillLoad,
SkillUnload,
PrePrompt,
PostResponse,
OnError,
}
impl std::fmt::Display for HookEventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HookEventType::PreToolUse => write!(f, "pre_tool_use"),
HookEventType::PostToolUse => write!(f, "post_tool_use"),
HookEventType::GenerateStart => write!(f, "generate_start"),
HookEventType::GenerateEnd => write!(f, "generate_end"),
HookEventType::SessionStart => write!(f, "session_start"),
HookEventType::SessionEnd => write!(f, "session_end"),
HookEventType::SkillLoad => write!(f, "skill_load"),
HookEventType::SkillUnload => write!(f, "skill_unload"),
HookEventType::PrePrompt => write!(f, "pre_prompt"),
HookEventType::PostResponse => write!(f, "post_response"),
HookEventType::OnError => write!(f, "on_error"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResultData {
pub success: bool,
pub output: String,
pub exit_code: Option<i32>,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreToolUseEvent {
pub session_id: String,
pub tool: String,
pub args: serde_json::Value,
pub working_directory: String,
pub recent_tools: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseEvent {
pub session_id: String,
pub tool: String,
pub args: serde_json::Value,
pub result: ToolResultData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateStartEvent {
pub session_id: String,
pub prompt: String,
pub system_prompt: Option<String>,
pub model_provider: String,
pub model_name: String,
pub available_tools: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateEndEvent {
pub session_id: String,
pub prompt: String,
pub response_text: String,
pub tool_calls: Vec<ToolCallInfo>,
pub usage: TokenUsageInfo,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallInfo {
pub name: String,
pub args: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenUsageInfo {
pub prompt_tokens: i32,
pub completion_tokens: i32,
pub total_tokens: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStartEvent {
pub session_id: String,
pub system_prompt: Option<String>,
pub model_provider: String,
pub model_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionEndEvent {
pub session_id: String,
pub total_tokens: i32,
pub total_tool_calls: i32,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillLoadEvent {
pub skill_name: String,
pub tool_names: Vec<String>,
pub version: Option<String>,
pub description: Option<String>,
pub loaded_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillUnloadEvent {
pub skill_name: String,
pub tool_names: Vec<String>,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrePromptEvent {
pub session_id: String,
pub prompt: String,
pub system_prompt: Option<String>,
pub message_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostResponseEvent {
pub session_id: String,
pub response_text: String,
pub tool_calls_count: usize,
pub usage: TokenUsageInfo,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorType {
ToolFailure,
LlmFailure,
PermissionDenied,
Timeout,
Other,
}
impl std::fmt::Display for ErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErrorType::ToolFailure => write!(f, "tool_failure"),
ErrorType::LlmFailure => write!(f, "llm_failure"),
ErrorType::PermissionDenied => write!(f, "permission_denied"),
ErrorType::Timeout => write!(f, "timeout"),
ErrorType::Other => write!(f, "other"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnErrorEvent {
pub session_id: String,
pub error_type: ErrorType,
pub error_message: String,
pub context: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event_type", content = "payload")]
pub enum HookEvent {
#[serde(rename = "pre_tool_use")]
PreToolUse(PreToolUseEvent),
#[serde(rename = "post_tool_use")]
PostToolUse(PostToolUseEvent),
#[serde(rename = "generate_start")]
GenerateStart(GenerateStartEvent),
#[serde(rename = "generate_end")]
GenerateEnd(GenerateEndEvent),
#[serde(rename = "session_start")]
SessionStart(SessionStartEvent),
#[serde(rename = "session_end")]
SessionEnd(SessionEndEvent),
#[serde(rename = "skill_load")]
SkillLoad(SkillLoadEvent),
#[serde(rename = "skill_unload")]
SkillUnload(SkillUnloadEvent),
#[serde(rename = "pre_prompt")]
PrePrompt(PrePromptEvent),
#[serde(rename = "post_response")]
PostResponse(PostResponseEvent),
#[serde(rename = "on_error")]
OnError(OnErrorEvent),
}
impl HookEvent {
pub fn event_type(&self) -> HookEventType {
match self {
HookEvent::PreToolUse(_) => HookEventType::PreToolUse,
HookEvent::PostToolUse(_) => HookEventType::PostToolUse,
HookEvent::GenerateStart(_) => HookEventType::GenerateStart,
HookEvent::GenerateEnd(_) => HookEventType::GenerateEnd,
HookEvent::SessionStart(_) => HookEventType::SessionStart,
HookEvent::SessionEnd(_) => HookEventType::SessionEnd,
HookEvent::SkillLoad(_) => HookEventType::SkillLoad,
HookEvent::SkillUnload(_) => HookEventType::SkillUnload,
HookEvent::PrePrompt(_) => HookEventType::PrePrompt,
HookEvent::PostResponse(_) => HookEventType::PostResponse,
HookEvent::OnError(_) => HookEventType::OnError,
}
}
pub fn session_id(&self) -> &str {
match self {
HookEvent::PreToolUse(e) => &e.session_id,
HookEvent::PostToolUse(e) => &e.session_id,
HookEvent::GenerateStart(e) => &e.session_id,
HookEvent::GenerateEnd(e) => &e.session_id,
HookEvent::SessionStart(e) => &e.session_id,
HookEvent::SessionEnd(e) => &e.session_id,
HookEvent::PrePrompt(e) => &e.session_id,
HookEvent::PostResponse(e) => &e.session_id,
HookEvent::OnError(e) => &e.session_id,
HookEvent::SkillLoad(_) => "",
HookEvent::SkillUnload(_) => "",
}
}
pub fn tool_name(&self) -> Option<&str> {
match self {
HookEvent::PreToolUse(e) => Some(&e.tool),
HookEvent::PostToolUse(e) => Some(&e.tool),
_ => None,
}
}
pub fn tool_args(&self) -> Option<&serde_json::Value> {
match self {
HookEvent::PreToolUse(e) => Some(&e.args),
HookEvent::PostToolUse(e) => Some(&e.args),
_ => None,
}
}
pub fn skill_name(&self) -> Option<&str> {
match self {
HookEvent::SkillLoad(e) => Some(&e.skill_name),
HookEvent::SkillUnload(e) => Some(&e.skill_name),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_event_type_display() {
assert_eq!(HookEventType::PreToolUse.to_string(), "pre_tool_use");
assert_eq!(HookEventType::PostToolUse.to_string(), "post_tool_use");
assert_eq!(HookEventType::GenerateStart.to_string(), "generate_start");
assert_eq!(HookEventType::GenerateEnd.to_string(), "generate_end");
assert_eq!(HookEventType::SessionStart.to_string(), "session_start");
assert_eq!(HookEventType::SessionEnd.to_string(), "session_end");
assert_eq!(HookEventType::SkillLoad.to_string(), "skill_load");
assert_eq!(HookEventType::SkillUnload.to_string(), "skill_unload");
}
#[test]
fn test_pre_tool_use_event() {
let event = PreToolUseEvent {
session_id: "session-1".to_string(),
tool: "Bash".to_string(),
args: serde_json::json!({"command": "echo hello"}),
working_directory: "/workspace".to_string(),
recent_tools: vec!["Read".to_string()],
};
assert_eq!(event.session_id, "session-1");
assert_eq!(event.tool, "Bash");
}
#[test]
fn test_post_tool_use_event() {
let event = PostToolUseEvent {
session_id: "session-1".to_string(),
tool: "Bash".to_string(),
args: serde_json::json!({"command": "echo hello"}),
result: ToolResultData {
success: true,
output: "hello\n".to_string(),
exit_code: Some(0),
duration_ms: 50,
},
};
assert!(event.result.success);
assert_eq!(event.result.exit_code, Some(0));
}
#[test]
fn test_hook_event_type() {
let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
session_id: "s1".to_string(),
tool: "Bash".to_string(),
args: serde_json::json!({}),
working_directory: "/".to_string(),
recent_tools: vec![],
});
assert_eq!(pre_tool.event_type(), HookEventType::PreToolUse);
assert_eq!(pre_tool.session_id(), "s1");
assert_eq!(pre_tool.tool_name(), Some("Bash"));
}
#[test]
fn test_hook_event_serialization() {
let event = HookEvent::PreToolUse(PreToolUseEvent {
session_id: "s1".to_string(),
tool: "Bash".to_string(),
args: serde_json::json!({"command": "ls"}),
working_directory: "/workspace".to_string(),
recent_tools: vec![],
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("pre_tool_use"));
assert!(json.contains("Bash"));
let parsed: HookEvent = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.event_type(), HookEventType::PreToolUse);
}
#[test]
fn test_generate_events() {
let start = GenerateStartEvent {
session_id: "s1".to_string(),
prompt: "Hello".to_string(),
system_prompt: Some("You are helpful".to_string()),
model_provider: "anthropic".to_string(),
model_name: "claude-3".to_string(),
available_tools: vec!["Bash".to_string(), "Read".to_string()],
};
let end = GenerateEndEvent {
session_id: "s1".to_string(),
prompt: "Hello".to_string(),
response_text: "Hi there!".to_string(),
tool_calls: vec![],
usage: TokenUsageInfo {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
duration_ms: 500,
};
assert_eq!(start.prompt, "Hello");
assert_eq!(end.response_text, "Hi there!");
assert_eq!(end.usage.total_tokens, 15);
}
#[test]
fn test_session_events() {
let start = SessionStartEvent {
session_id: "s1".to_string(),
system_prompt: Some("System".to_string()),
model_provider: "anthropic".to_string(),
model_name: "claude-3".to_string(),
};
let end = SessionEndEvent {
session_id: "s1".to_string(),
total_tokens: 1000,
total_tool_calls: 5,
duration_ms: 60000,
};
let start_event = HookEvent::SessionStart(start);
let end_event = HookEvent::SessionEnd(end);
assert_eq!(start_event.event_type(), HookEventType::SessionStart);
assert_eq!(end_event.event_type(), HookEventType::SessionEnd);
assert!(start_event.tool_name().is_none());
}
#[test]
fn test_skill_load_event() {
let event = SkillLoadEvent {
skill_name: "test-skill".to_string(),
tool_names: vec!["tool1".to_string(), "tool2".to_string()],
version: Some("1.0.0".to_string()),
description: Some("A test skill".to_string()),
loaded_at: 1234567890,
};
assert_eq!(event.skill_name, "test-skill");
assert_eq!(event.tool_names.len(), 2);
assert_eq!(event.version, Some("1.0.0".to_string()));
assert_eq!(event.loaded_at, 1234567890);
}
#[test]
fn test_skill_unload_event() {
let event = SkillUnloadEvent {
skill_name: "test-skill".to_string(),
tool_names: vec!["tool1".to_string(), "tool2".to_string()],
duration_ms: 60000,
};
assert_eq!(event.skill_name, "test-skill");
assert_eq!(event.tool_names.len(), 2);
assert_eq!(event.duration_ms, 60000);
}
#[test]
fn test_hook_event_skill_name() {
let load_event = HookEvent::SkillLoad(SkillLoadEvent {
skill_name: "my-skill".to_string(),
tool_names: vec!["tool1".to_string()],
version: None,
description: None,
loaded_at: 0,
});
let unload_event = HookEvent::SkillUnload(SkillUnloadEvent {
skill_name: "my-skill".to_string(),
tool_names: vec!["tool1".to_string()],
duration_ms: 1000,
});
assert_eq!(load_event.event_type(), HookEventType::SkillLoad);
assert_eq!(load_event.skill_name(), Some("my-skill"));
assert_eq!(load_event.session_id(), "");
assert_eq!(unload_event.event_type(), HookEventType::SkillUnload);
assert_eq!(unload_event.skill_name(), Some("my-skill"));
assert_eq!(unload_event.session_id(), "");
let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
session_id: "s1".to_string(),
tool: "Bash".to_string(),
args: serde_json::json!({}),
working_directory: "/".to_string(),
recent_tools: vec![],
});
assert!(pre_tool.skill_name().is_none());
}
#[test]
fn test_skill_event_serialization() {
let event = HookEvent::SkillLoad(SkillLoadEvent {
skill_name: "test-skill".to_string(),
tool_names: vec!["tool1".to_string()],
version: Some("1.0.0".to_string()),
description: None,
loaded_at: 1234567890,
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("skill_load"));
assert!(json.contains("test-skill"));
assert!(json.contains("1.0.0"));
let parsed: HookEvent = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.event_type(), HookEventType::SkillLoad);
assert_eq!(parsed.skill_name(), Some("test-skill"));
}
#[test]
fn test_hook_event_type_display_new_variants() {
assert_eq!(HookEventType::PrePrompt.to_string(), "pre_prompt");
assert_eq!(HookEventType::PostResponse.to_string(), "post_response");
assert_eq!(HookEventType::OnError.to_string(), "on_error");
}
#[test]
fn test_pre_prompt_event() {
let event = PrePromptEvent {
session_id: "s1".to_string(),
prompt: "Fix the bug".to_string(),
system_prompt: Some("You are helpful".to_string()),
message_count: 5,
};
assert_eq!(event.session_id, "s1");
assert_eq!(event.prompt, "Fix the bug");
assert_eq!(event.message_count, 5);
let hook_event = HookEvent::PrePrompt(event);
assert_eq!(hook_event.event_type(), HookEventType::PrePrompt);
assert_eq!(hook_event.session_id(), "s1");
assert!(hook_event.tool_name().is_none());
assert!(hook_event.skill_name().is_none());
}
#[test]
fn test_post_response_event() {
let event = PostResponseEvent {
session_id: "s1".to_string(),
response_text: "Done!".to_string(),
tool_calls_count: 3,
usage: TokenUsageInfo {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
},
duration_ms: 2000,
};
assert_eq!(event.response_text, "Done!");
assert_eq!(event.tool_calls_count, 3);
assert_eq!(event.usage.total_tokens, 150);
let hook_event = HookEvent::PostResponse(event);
assert_eq!(hook_event.event_type(), HookEventType::PostResponse);
assert_eq!(hook_event.session_id(), "s1");
}
#[test]
fn test_on_error_event() {
let event = OnErrorEvent {
session_id: "s1".to_string(),
error_type: ErrorType::ToolFailure,
error_message: "Command failed with exit code 1".to_string(),
context: serde_json::json!({"tool": "Bash", "command": "false"}),
};
assert_eq!(event.error_type.to_string(), "tool_failure");
assert_eq!(event.error_message, "Command failed with exit code 1");
let hook_event = HookEvent::OnError(event);
assert_eq!(hook_event.event_type(), HookEventType::OnError);
assert_eq!(hook_event.session_id(), "s1");
}
#[test]
fn test_error_type_display() {
assert_eq!(ErrorType::ToolFailure.to_string(), "tool_failure");
assert_eq!(ErrorType::LlmFailure.to_string(), "llm_failure");
assert_eq!(ErrorType::PermissionDenied.to_string(), "permission_denied");
assert_eq!(ErrorType::Timeout.to_string(), "timeout");
assert_eq!(ErrorType::Other.to_string(), "other");
}
#[test]
fn test_new_event_serialization() {
let event = HookEvent::PrePrompt(PrePromptEvent {
session_id: "s1".to_string(),
prompt: "Hello".to_string(),
system_prompt: None,
message_count: 0,
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("pre_prompt"));
let parsed: HookEvent = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.event_type(), HookEventType::PrePrompt);
let event = HookEvent::PostResponse(PostResponseEvent {
session_id: "s1".to_string(),
response_text: "Hi".to_string(),
tool_calls_count: 0,
usage: TokenUsageInfo {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
duration_ms: 100,
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("post_response"));
let parsed: HookEvent = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.event_type(), HookEventType::PostResponse);
let event = HookEvent::OnError(OnErrorEvent {
session_id: "s1".to_string(),
error_type: ErrorType::LlmFailure,
error_message: "API timeout".to_string(),
context: serde_json::json!({}),
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("on_error"));
let parsed: HookEvent = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.event_type(), HookEventType::OnError);
}
}