Hook Development Guide
This guide will help you create custom hooks for Radium. Hooks allow you to intercept and customize behavior at various points in the execution flow.
Overviewβ
Hooks in Radium implement the Hook trait and are registered with the HookRegistry. They can be implemented in Rust and loaded dynamically, or configured via TOML for simpler use cases.
Creating a Hookβ
Step 1: Create a New Cargo Projectβ
Create a new library crate for your hook:
cargo new --lib my-hook
cd my-hook
Step 2: Add Dependenciesβ
Update your Cargo.toml:
[package]
name = "my-hook"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_hook"
crate-type = ["cdylib", "rlib"]
[dependencies]
radium-core = { path = "../../../crates/radium-core" }
async-trait = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
Step 3: Implement the Hook Traitβ
For model hooks, implement the ModelHook trait:
use async_trait::async_trait;
use radium_core::hooks::model::{ModelHook, ModelHookContext};
use radium_core::hooks::types::{HookPriority, HookResult};
use radium_core::hooks::error::Result;
pub struct MyModelHook {
name: String,
priority: HookPriority,
}
impl MyModelHook {
pub fn new(name: impl Into<String>, priority: u32) -> Self {
Self {
name: name.into(),
priority: HookPriority::new(priority),
}
}
}
#[async_trait]
impl ModelHook for MyModelHook {
fn name(&self) -> &str {
&self.name
}
fn priority(&self) -> HookPriority {
self.priority
}
async fn before_model_call(&self, context: &ModelHookContext) -> Result<HookResult> {
// Your logic here
tracing::info!("Before model call: {}", context.model_id);
Ok(HookResult::success())
}
async fn after_model_call(&self, context: &ModelHookContext) -> Result<HookResult> {
// Your logic here
tracing::info!("After model call: {}", context.model_id);
Ok(HookResult::success())
}
}
For telemetry hooks, implement the Hook trait directly:
use async_trait::async_trait;
use radium_core::hooks::registry::{Hook, HookType};
use radium_core::hooks::types::{HookContext, HookPriority, HookResult};
use radium_core::hooks::error::Result;
pub struct MyTelemetryHook {
name: String,
priority: HookPriority,
}
#[async_trait]
impl Hook for MyTelemetryHook {
fn name(&self) -> &str {
&self.name
}
fn priority(&self) -> HookPriority {
self.priority
}
fn hook_type(&self) -> HookType {
HookType::TelemetryCollection
}
async fn execute(&self, context: &HookContext) -> Result<HookResult> {
// Extract telemetry data from context.data
let data = &context.data;
// Your logic here
Ok(HookResult::success())
}
}
Step 4: Create Hook Adaptersβ
For model hooks, use ModelHookAdapter:
use radium_core::hooks::model::ModelHookAdapter;
use std::sync::Arc;
pub fn create_before_hook() -> Arc<dyn radium_core::hooks::registry::Hook> {
let hook = Arc::new(MyModelHook::new("my-hook-before", 100));
ModelHookAdapter::before(hook)
}
pub fn create_after_hook() -> Arc<dyn radium_core::hooks::registry::Hook> {
let hook = Arc::new(MyModelHook::new("my-hook-after", 100));
ModelHookAdapter::after(hook)
}
Step 5: Build and Packageβ
Build your hook:
cargo build --release
The compiled library can be loaded dynamically or linked statically.
Hook Contextsβ
Different hook types receive different context data:
Model Hook Contextβ
pub struct ModelHookContext {
pub input: String,
pub model_id: String,
pub request_modifications: Option<serde_json::Value>,
pub response: Option<String>, // Only for after hooks
pub modified_input: Option<String>,
}
Tool Hook Contextβ
pub struct ToolHookContext {
pub tool_name: String,
pub arguments: serde_json::Value,
pub result: Option<serde_json::Value>, // Only for after hooks
pub modified_arguments: Option<serde_json::Value>,
pub modified_result: Option<serde_json::Value>,
}
Telemetry Hook Contextβ
Telemetry data is passed as JSON in HookContext.data:
{
"agent_id": "agent-123",
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150,
"estimated_cost": 0.001,
"model": "gpt-4",
"provider": "openai"
}
Hook Resultsβ
Hooks return HookResult which can:
- Continue execution:
HookResult::success() - Modify data:
HookResult::with_data(modified_data) - Stop execution:
HookResult::stop("reason") - Report error but continue:
HookResult::error("message")
Modifying Execution Flowβ
To modify input/output:
// Modify input in before_model hook
let modified_input = format!("Prefix: {}", context.input);
let modified_data = json!({
"input": modified_input,
});
Ok(HookResult::with_data(modified_data))
To stop execution:
// Stop execution if condition is met
if some_condition {
Ok(HookResult::stop("Execution stopped by hook"))
} else {
Ok(HookResult::success())
}
Testing Hooksβ
Write unit tests for your hooks:
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_my_hook() {
let hook = MyModelHook::new("test-hook", 100);
let context = ModelHookContext::before(
"test input".to_string(),
"test-model".to_string(),
);
let result = hook.before_model_call(&context).await.unwrap();
assert!(result.success);
assert!(result.should_continue);
}
}
Best Practicesβ
-
Error Handling: Always handle errors gracefully. Don't let hook failures crash the system.
-
Performance: Keep hook execution fast. Long-running operations should be async.
-
Thread Safety: Hooks must be
Send + Sync. UseArcandRwLockfor shared state. -
Logging: Use the
tracingcrate for logging. Don't useprintln!. -
Priority: Choose appropriate priorities. Critical hooks should have high priority.
-
Idempotency: Hooks should be idempotent when possible.
-
Configuration: Use
HookConfigfor configurable behavior.
Example Implementationsβ
See the example hooks in examples/hooks/:
logging-hook: Logs model callsmetrics-hook: Aggregates telemetry data
Registering Hooksβ
Programmatic Registrationβ
Register hooks directly in your code:
use radium_core::hooks::registry::HookRegistry;
use std::sync::Arc;
let registry = Arc::new(HookRegistry::new());
let hook = Arc::new(MyModelHook::new("my-hook", 100));
let adapter = ModelHookAdapter::before(hook);
registry.register(adapter).await?;
Configuration-Based Registrationβ
For v1.0, hooks must be registered programmatically, but configuration controls enable/disable state:
- Create
.radium/hooks.toml:
[[hooks]]
name = "my-hook"
type = "before_model"
priority = 100
enabled = true
- Register the hook programmatically, then load configuration:
// Register hook
let hook = Arc::new(MyModelHook::new("my-hook", 100));
registry.register(ModelHookAdapter::before(hook)).await?;
// Load configuration (sets enabled state)
HookLoader::load_from_workspace(workspace_root, ®istry).await?;
The configuration will set the enabled/disabled state of already-registered hooks.
Extension Integrationβ
To distribute your hook via extensions:
- Create an extension manifest (
radium-extension.json) - Add hook configuration files to the extension
- Package the extension
Example manifest:
{
"name": "my-hook-extension",
"version": "1.0.0",
"description": "My custom hook",
"author": "Your Name",
"components": {
"hooks": ["hooks/*.toml"]
}
}
Note: For v1.0, hooks must be registered programmatically. Configuration files control enable/disable state. Dynamic library loading is deferred to v2.0.
Debuggingβ
Enable debug logging:
RUST_LOG=radium_core::hooks=debug rad <command>
Check hook registration:
rad hooks list
rad hooks info my-hook
Next Stepsβ
- See API Reference for complete API documentation
- See Getting Started for usage examples
- Check out example implementations in
examples/hooks/