use std::sync::Arc;
use super::types::{ExecutionMode, SubagentResult};
#[derive(Debug, Clone)]
pub struct SubagentHookContext {
pub parent_agent: String,
pub subagent_name: String,
pub execution_mode: ExecutionMode,
pub task: String,
pub attempt: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubagentRetryDecision {
Retry {
delay_secs: u64,
},
Fail,
Delegate {
alternative_agent: String,
},
}
#[async_trait::async_trait]
pub trait SubagentHooks: Send + Sync {
async fn before_dispatch(&self, _ctx: &SubagentHookContext) {}
async fn after_dispatch(&self, _ctx: &SubagentHookContext, _result: &SubagentResult) {}
async fn on_failure(&self, _ctx: &SubagentHookContext, _error: &str) -> SubagentRetryDecision {
SubagentRetryDecision::Fail
}
async fn on_cancelled(&self, _ctx: &SubagentHookContext) {}
}
pub struct NoopSubagentHooks;
#[async_trait::async_trait]
impl SubagentHooks for NoopSubagentHooks {}
pub struct LoggingSubagentHooks;
#[async_trait::async_trait]
impl SubagentHooks for LoggingSubagentHooks {
async fn before_dispatch(&self, ctx: &SubagentHookContext) {
tracing::info!(
parent = %ctx.parent_agent,
subagent = %ctx.subagent_name,
mode = %ctx.execution_mode,
task = %ctx.task,
"subagent_before_dispatch"
);
}
async fn after_dispatch(&self, ctx: &SubagentHookContext, result: &SubagentResult) {
let preview = if result.output.len() > 100 {
format!("{}...", &result.output[..100])
} else {
result.output.clone()
};
tracing::info!(
parent = %ctx.parent_agent,
subagent = %ctx.subagent_name,
duration_ms = result.duration.as_millis(),
output = %preview,
"subagent_after_dispatch"
);
}
async fn on_failure(&self, ctx: &SubagentHookContext, error: &str) -> SubagentRetryDecision {
tracing::warn!(
parent = %ctx.parent_agent,
subagent = %ctx.subagent_name,
attempt = ctx.attempt,
error = %error,
"subagent_on_failure"
);
SubagentRetryDecision::Fail
}
async fn on_cancelled(&self, ctx: &SubagentHookContext) {
tracing::info!(
parent = %ctx.parent_agent,
subagent = %ctx.subagent_name,
"subagent_on_cancelled"
);
}
}
pub struct SubagentHookRegistry {
hooks: Vec<Arc<dyn SubagentHooks>>,
}
impl SubagentHookRegistry {
pub fn new() -> Self {
Self { hooks: Vec::new() }
}
pub fn with_logging() -> Self {
let mut registry = Self::new();
registry.register(Arc::new(LoggingSubagentHooks));
registry
}
pub fn register(&mut self, hook: Arc<dyn SubagentHooks>) {
self.hooks.push(hook);
}
pub async fn before_dispatch(&self, ctx: &SubagentHookContext) {
for hook in &self.hooks {
hook.before_dispatch(ctx).await;
}
}
pub async fn after_dispatch(&self, ctx: &SubagentHookContext, result: &SubagentResult) {
for hook in &self.hooks {
hook.after_dispatch(ctx, result).await;
}
}
pub async fn on_failure(
&self,
ctx: &SubagentHookContext,
error: &str,
) -> SubagentRetryDecision {
for hook in &self.hooks {
let decision = hook.on_failure(ctx, error).await;
if !matches!(decision, SubagentRetryDecision::Fail) {
return decision;
}
}
SubagentRetryDecision::Fail
}
pub async fn on_cancelled(&self, ctx: &SubagentHookContext) {
for hook in &self.hooks {
hook.on_cancelled(ctx).await;
}
}
pub fn is_empty(&self) -> bool {
self.hooks.is_empty()
}
pub fn len(&self) -> usize {
self.hooks.len()
}
}
impl Default for SubagentHookRegistry {
fn default() -> Self {
Self::new()
}
}
impl Clone for SubagentHookRegistry {
fn clone(&self) -> Self {
Self {
hooks: self.hooks.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
struct TestHooks {
before: AtomicU32,
after: AtomicU32,
failure: AtomicU32,
}
impl TestHooks {
fn new() -> Self {
Self {
before: AtomicU32::new(0),
after: AtomicU32::new(0),
failure: AtomicU32::new(0),
}
}
}
#[async_trait::async_trait]
impl SubagentHooks for TestHooks {
async fn before_dispatch(&self, _ctx: &SubagentHookContext) {
self.before.fetch_add(1, Ordering::SeqCst);
}
async fn after_dispatch(&self, _ctx: &SubagentHookContext, _result: &SubagentResult) {
self.after.fetch_add(1, Ordering::SeqCst);
}
async fn on_failure(
&self,
_ctx: &SubagentHookContext,
_error: &str,
) -> SubagentRetryDecision {
self.failure.fetch_add(1, Ordering::SeqCst);
SubagentRetryDecision::Retry { delay_secs: 1 }
}
}
fn make_ctx() -> SubagentHookContext {
SubagentHookContext {
parent_agent: "parent".into(),
subagent_name: "child".into(),
execution_mode: ExecutionMode::Sync,
task: "test task".into(),
attempt: 1,
}
}
#[tokio::test]
async fn test_hooks_called() {
let hooks = Arc::new(TestHooks::new());
let mut registry = SubagentHookRegistry::new();
registry.register(hooks.clone());
let ctx = make_ctx();
let result = SubagentResult::sync_result("child", "ok".into(), Duration::from_millis(100));
registry.before_dispatch(&ctx).await;
registry.after_dispatch(&ctx, &result).await;
registry.on_failure(&ctx, "error").await;
assert_eq!(hooks.before.load(Ordering::SeqCst), 1);
assert_eq!(hooks.after.load(Ordering::SeqCst), 1);
assert_eq!(hooks.failure.load(Ordering::SeqCst), 1);
}
#[test]
fn test_registry_default() {
let registry = SubagentHookRegistry::default();
assert!(registry.is_empty());
}
#[test]
fn test_registry_with_logging() {
let registry = SubagentHookRegistry::with_logging();
assert_eq!(registry.len(), 1);
}
}