use crate::runtime::{ProviderRequest, RunOutput};
pub trait LifecycleHook: Send + Sync {
fn pre_tool_call(&self, _tool_name: &str, _input: &serde_json::Value) {}
fn post_tool_call(&self, _tool_name: &str, _result: &str) {}
fn pre_llm_call(&self, _request: &ProviderRequest) {}
fn post_llm_call(&self, _request: &ProviderRequest) {}
fn on_session_start(&self, _session_id: &str) {}
fn on_session_end(&self, _session_id: &str, _output: &RunOutput) {}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
struct CountingHook {
pre_tool: AtomicU32,
post_tool: AtomicU32,
pre_llm: AtomicU32,
post_llm: AtomicU32,
session_start: AtomicU32,
session_end: AtomicU32,
}
impl CountingHook {
fn new() -> Self {
Self {
pre_tool: AtomicU32::new(0),
post_tool: AtomicU32::new(0),
pre_llm: AtomicU32::new(0),
post_llm: AtomicU32::new(0),
session_start: AtomicU32::new(0),
session_end: AtomicU32::new(0),
}
}
}
impl LifecycleHook for CountingHook {
fn pre_tool_call(&self, _tool_name: &str, _input: &serde_json::Value) {
self.pre_tool.fetch_add(1, Ordering::Relaxed);
}
fn post_tool_call(&self, _tool_name: &str, _result: &str) {
self.post_tool.fetch_add(1, Ordering::Relaxed);
}
fn pre_llm_call(&self, _request: &ProviderRequest) {
self.pre_llm.fetch_add(1, Ordering::Relaxed);
}
fn post_llm_call(&self, _request: &ProviderRequest) {
self.post_llm.fetch_add(1, Ordering::Relaxed);
}
fn on_session_start(&self, _session_id: &str) {
self.session_start.fetch_add(1, Ordering::Relaxed);
}
fn on_session_end(&self, _session_id: &str, _output: &RunOutput) {
self.session_end.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn default_impls_are_noop() {
struct NoopHook;
impl LifecycleHook for NoopHook {}
let hook = NoopHook;
hook.pre_tool_call("test", &serde_json::json!({}));
hook.post_tool_call("test", "ok");
}
#[test]
fn counting_hook_tracks_invocations() {
let hook = Arc::new(CountingHook::new());
hook.pre_tool_call("bash", &serde_json::json!({"command": "ls"}));
hook.pre_tool_call("read_file", &serde_json::json!({"path": "/tmp"}));
hook.post_tool_call("bash", "file1.txt");
assert_eq!(hook.pre_tool.load(Ordering::Relaxed), 2);
assert_eq!(hook.post_tool.load(Ordering::Relaxed), 1);
assert_eq!(hook.pre_llm.load(Ordering::Relaxed), 0);
assert_eq!(hook.post_llm.load(Ordering::Relaxed), 0);
assert_eq!(hook.session_start.load(Ordering::Relaxed), 0);
assert_eq!(hook.session_end.load(Ordering::Relaxed), 0);
}
#[test]
fn hook_is_send_sync() {
fn assert_send_sync<T: Send + Sync + ?Sized>() {}
assert_send_sync::<dyn LifecycleHook>();
}
#[test]
fn session_lifecycle_events_fire() {
let hook = Arc::new(CountingHook::new());
hook.on_session_start("session-1");
hook.on_session_start("session-1");
assert_eq!(hook.session_start.load(Ordering::Relaxed), 2);
assert_eq!(hook.session_end.load(Ordering::Relaxed), 0);
}
}