Hook Development Best Practices
This document outlines best practices and anti-patterns for developing hooks in Radium.
Do's β β
Keep Hooks Focused on Single Responsibilityβ
Good:
pub struct LoggingHook {
// Only handles logging
}
impl ModelHook for LoggingHook {
async fn before_model_call(&self, context: &ModelHookContext) -> Result<HookExecutionResult> {
tracing::info!("Model call: {}", context.model_id);
Ok(HookExecutionResult::success())
}
}
Bad:
pub struct LoggingAndValidationHook {
// Does too much: logging AND validation
}
impl ModelHook for LoggingAndValidationHook {
async fn before_model_call(&self, context: &ModelHookContext) -> Result<HookExecutionResult> {
// Logging
tracing::info!("Model call: {}", context.model_id);
// Validation (should be separate hook)
if context.input.is_empty() {
return Ok(HookExecutionResult::stop("Empty input"));
}
// Metrics (should be separate hook)
self.metrics.increment();
Ok(HookExecutionResult::success())
}
}
Use Appropriate Priority Levelsβ
Guidelines:
- High Priority (200+): Security checks, critical validation, access control
- Medium Priority (100-199): Standard operations, logging, transformation
- Low Priority (<100): Optional monitoring, non-critical telemetry
Good:
// Security hook - high priority
pub struct SecurityHook {
priority: HookPriority::new(250),
}
// Logging hook - medium priority
pub struct LoggingHook {
priority: HookPriority::new(100),
}
// Metrics hook - low priority
pub struct MetricsHook {
priority: HookPriority::new(50),
}
Bad:
// Security hook with low priority - runs after other hooks!
pub struct SecurityHook {
priority: HookPriority::new(50), // Too low!
}
Handle Errors Gracefullyβ
Good:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
match self.process(context).await {
Ok(_) => Ok(HookExecutionResult::success()),
Err(e) => {
// Log error but don't crash
tracing::warn!(error = %e, "Hook processing failed");
// Return success to allow other hooks to run
Ok(HookExecutionResult::error(format!("Processing failed: {}", e)))
}
}
}
Bad:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Panics on error - crashes entire system!
self.process(context).await.unwrap();
Ok(HookExecutionResult::success())
}
Test Hooks in Isolationβ
Good:
#[tokio::test]
async fn test_hook_in_isolation() {
let hook = MyHook::new("test", 100);
let context = create_test_context();
let result = hook.execute(&context).await.unwrap();
assert!(result.success);
}
Bad:
// Testing with real registry and other hooks - not isolated
#[tokio::test]
async fn test_hook_with_registry() {
let registry = HookRegistry::new();
registry.register(hook1).await?;
registry.register(hook2).await?;
registry.register(my_hook).await?; // Hard to test in isolation
// ...
}
Use Thread-Safe Patternsβ
Good:
pub struct SharedStateHook {
state: Arc<RwLock<HashMap<String, u64>>>,
}
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
let mut state = self.state.write().await; // Proper locking
state.insert(key, value);
Ok(HookExecutionResult::success())
}
Bad:
pub struct UnsafeStateHook {
state: HashMap<String, u64>, // Not thread-safe!
}
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
self.state.insert(key, value); // Race condition!
Ok(HookExecutionResult::success())
}
Don'ts ββ
Don't Perform Blocking Operationsβ
Bad:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Blocking I/O in async function!
std::thread::sleep(Duration::from_secs(1));
std::fs::write("file.txt", "data").unwrap();
Ok(HookExecutionResult::success())
}
Good:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Async I/O
tokio::time::sleep(Duration::from_secs(1)).await;
tokio::fs::write("file.txt", "data").await?;
Ok(HookExecutionResult::success())
}
Don't Modify Shared State Without Synchronizationβ
Bad:
pub struct UnsafeCounterHook {
counter: u64, // Not synchronized!
}
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
self.counter += 1; // Race condition!
Ok(HookExecutionResult::success())
}
Good:
pub struct SafeCounterHook {
counter: Arc<RwLock<u64>>, // Synchronized
}
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
let mut counter = self.counter.write().await;
*counter += 1;
Ok(HookExecutionResult::success())
}
Don't Ignore Hook Execution Failuresβ
Bad:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Ignores errors silently
let _ = self.process(context).await;
Ok(HookExecutionResult::success())
}
Good:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
match self.process(context).await {
Ok(_) => Ok(HookExecutionResult::success()),
Err(e) => {
tracing::warn!(error = %e, "Hook processing failed");
Ok(HookExecutionResult::error(e.to_string()))
}
}
}
Don't Use High Priority for Non-Critical Operationsβ
Bad:
// Logging doesn't need high priority
pub struct LoggingHook {
priority: HookPriority::new(250), // Too high!
}
Good:
// Logging with appropriate priority
pub struct LoggingHook {
priority: HookPriority::new(100), // Appropriate
}
Don't Store Large Data in Contextβ
Bad:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Storing large data in context
let large_data = vec![0u8; 10_000_000];
Ok(HookExecutionResult::with_data(json!({
"large_data": large_data // Too large!
})))
}
Good:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Store reference or summary instead
let summary = calculate_summary(&large_data);
Ok(HookExecutionResult::with_data(json!({
"summary": summary
})))
}
Hook Type Selectionβ
When to Use Model Hooksβ
- Input/output validation
- Request/response transformation
- Logging model calls
- Cost tracking
- Rate limiting
When to Use Tool Hooksβ
- Tool argument validation
- Security checks
- Tool execution logging
- Tool result transformation
- Access control
When to Use Error Hooksβ
- Error transformation
- Error recovery
- Error logging
- Error notification
- Error aggregation
When to Use Telemetry Hooksβ
- Metrics collection
- Performance monitoring
- Cost tracking
- Usage analytics
- Custom logging
Priority Selection Guidelinesβ
High Priority (200+)β
Use for:
- Security checks
- Critical validation
- Access control
- Safety checks
Example:
pub struct SecurityHook {
priority: HookPriority::new(250),
}
Medium Priority (100-199)β
Use for:
- Standard operations
- Logging
- Transformation
- Standard validation
Example:
pub struct LoggingHook {
priority: HookPriority::new(100),
}
Low Priority (<100)β
Use for:
- Optional monitoring
- Non-critical telemetry
- Background tasks
- Caching
Example:
pub struct MetricsHook {
priority: HookPriority::new(50),
}
Performance Considerationsβ
Keep Hook Execution Fastβ
Good:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Fast operation
let result = self.cache.get(&key).await;
Ok(HookExecutionResult::success())
}
Bad:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
// Slow operation blocks execution
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(HookExecutionResult::success())
}
Use Async Operationsβ
Good:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
tokio::fs::read("file.txt").await?;
Ok(HookExecutionResult::success())
}
Bad:
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
std::fs::read("file.txt")?; // Blocking!
Ok(HookExecutionResult::success())
}
Cache Expensive Operationsβ
Good:
pub struct CachedHook {
cache: Arc<RwLock<HashMap<String, String>>>,
}
async fn execute(&self, context: &HookContext) -> Result<HookExecutionResult> {
let key = context.data.get("key").and_then(|v| v.as_str()).unwrap();
// Check cache first
if let Some(cached) = self.cache.read().await.get(key) {
return Ok(HookExecutionResult::with_data(json!({
"result": cached
})));
}
// Expensive operation only if not cached
let result = expensive_operation().await?;
self.cache.write().await.insert(key.to_string(), result.clone());
Ok(HookExecutionResult::with_data(json!({
"result": result
})))
}
Summaryβ
- β Keep hooks focused on single responsibility
- β Use appropriate priority levels
- β Handle errors gracefully
- β Test hooks in isolation
- β Use thread-safe patterns
- β Don't perform blocking operations
- β Don't modify shared state without synchronization
- β Don't ignore hook execution failures
- β Don't use high priority for non-critical operations
- β Don't store large data in context