use std::sync::Arc;
use std::time::Duration;
use defect_agent::hooks::builtin::BuiltinRegistry;
use defect_agent::hooks::command::{CommandHandler, CommandSpec, ShellKind as AgentShellKind};
use defect_agent::hooks::prompt::{PromptHandler, PromptRender as AgentPromptRender, PromptSpec};
use defect_agent::hooks::{
DefaultHookEngine, HandlerTable, HookMatcher as AgentHookMatcher, StepHandler, StepHandlerEntry,
};
use defect_agent::llm::{LlmProvider, ProviderRegistry};
use defect_config::{
HookCommandSpec, HookHandlerSpec, HookMatcher as ConfigHookMatcher, HookPromptRender,
HookPromptSpec, HookShellKind, HooksConfig,
};
#[derive(Debug, thiserror::Error)]
pub enum HookEngineBuildError {
#[error("unknown builtin hook handler `{name}` (available: {available})")]
UnknownBuiltin { name: String, available: String },
#[error("hook configuration invalid: {0}")]
Configuration(String),
}
pub struct HookEngineCtx<'a> {
pub registry: &'a Arc<ProviderRegistry>,
pub default_model: &'a str,
}
fn build_handler_table(
hooks: &HooksConfig,
builtins: &BuiltinRegistry,
rt: &HookEngineCtx<'_>,
) -> Result<HandlerTable, HookEngineBuildError> {
let mut table = HandlerTable::empty();
for (event_name, entries) in &hooks.buckets {
let Some(static_name) = static_event_name(event_name) else {
continue;
};
for entry in entries {
let matcher = translate_matcher(&entry.matcher);
let (handler, timeout) = build_handler(&entry.handler, builtins, rt)?;
let mut hook = StepHandlerEntry::new(matcher, handler).with_name(entry.name.clone());
if let Some(t) = timeout {
hook = hook.with_timeout(t);
}
table.push_step(static_name, hook);
}
}
Ok(table)
}
pub fn build_hook_engine(
hooks: &HooksConfig,
builtins: &BuiltinRegistry,
rt: &HookEngineCtx<'_>,
) -> Result<DefaultHookEngine, HookEngineBuildError> {
let table = build_handler_table(hooks, builtins, rt)?;
let engine = DefaultHookEngine::new();
engine.reload(table);
Ok(engine)
}
fn static_event_name(name: &str) -> Option<&'static str> {
defect_agent::hooks::step::ALL_EVENT_NAMES
.iter()
.copied()
.find(|&n| n == name)
}
fn build_handler(
spec: &HookHandlerSpec,
builtins: &BuiltinRegistry,
rt: &HookEngineCtx<'_>,
) -> Result<(Arc<dyn StepHandler>, Option<Duration>), HookEngineBuildError> {
match spec {
HookHandlerSpec::Builtin { name } => {
let handler = builtins.lookup_step(name).ok_or_else(|| {
let available = builtins.names().collect::<Vec<_>>().join(", ");
HookEngineBuildError::UnknownBuiltin {
name: name.clone(),
available,
}
})?;
Ok((handler, None))
}
HookHandlerSpec::Command(cmd) => {
let agent_spec = translate_command(cmd);
let handler = CommandHandler::new(agent_spec);
let timeout = handler.timeout();
Ok((Arc::new(handler) as Arc<dyn StepHandler>, timeout))
}
HookHandlerSpec::Prompt(prompt) => {
let provider = resolve_prompt_provider(prompt, rt)?;
let agent_spec = translate_prompt(prompt, provider, rt.default_model.to_string());
let handler = PromptHandler::new(agent_spec);
let timeout = handler.timeout();
Ok((Arc::new(handler) as Arc<dyn StepHandler>, timeout))
}
other => Err(HookEngineBuildError::Configuration(format!(
"unrecognized hook handler form: {other:?}"
))),
}
}
fn resolve_prompt_provider(
spec: &HookPromptSpec,
rt: &HookEngineCtx<'_>,
) -> Result<Arc<dyn LlmProvider>, HookEngineBuildError> {
let model_id = spec.model.as_deref().unwrap_or(rt.default_model);
let entry = rt.registry.first_entry_for_model(model_id).ok_or_else(|| {
HookEngineBuildError::Configuration(format!(
"prompt hook references unknown model `{model_id}` (no provider registered for it)"
))
})?;
Ok(Arc::clone(entry.provider()))
}
fn translate_matcher(m: &ConfigHookMatcher) -> AgentHookMatcher {
let mut out = AgentHookMatcher::default();
out.tool = m.tool.clone();
out.tool_glob = m.tool_glob.clone();
out.safety = m.safety.clone();
out
}
fn translate_command(spec: &HookCommandSpec) -> CommandSpec {
match spec {
HookCommandSpec::Argv {
argv,
argv_windows,
cwd,
env,
timeout_sec,
} => CommandSpec::Argv {
argv: argv.clone(),
argv_windows: argv_windows.clone(),
cwd: cwd.clone(),
env: env.clone(),
timeout_sec: *timeout_sec,
},
HookCommandSpec::Shell {
shell,
command,
cwd,
env,
timeout_sec,
} => CommandSpec::Shell {
shell: translate_shell(shell),
command: command.clone(),
cwd: cwd.clone(),
env: env.clone(),
timeout_sec: *timeout_sec,
},
other => {
let _ = other;
CommandSpec::Argv {
argv: Vec::new(),
argv_windows: None,
cwd: None,
env: Default::default(),
timeout_sec: None,
}
}
}
}
fn translate_shell(shell: &HookShellKind) -> AgentShellKind {
match shell {
HookShellKind::Sh => AgentShellKind::Sh,
HookShellKind::Bash => AgentShellKind::Bash,
HookShellKind::Pwsh => AgentShellKind::Pwsh,
HookShellKind::Cmd => AgentShellKind::Cmd,
HookShellKind::Custom { program, args } => AgentShellKind::Custom {
program: program.clone(),
args: args.clone(),
},
other => {
let _ = other;
AgentShellKind::Sh
}
}
}
fn translate_prompt(
spec: &HookPromptSpec,
provider: Arc<dyn LlmProvider>,
fallback_model: String,
) -> PromptSpec {
PromptSpec {
provider,
model: spec.model.clone(),
fallback_model,
system: spec.system.clone(),
render: match &spec.render {
HookPromptRender::Json => AgentPromptRender::Json,
HookPromptRender::Template { template } => AgentPromptRender::Template {
template: template.clone(),
},
other => {
let _ = other;
AgentPromptRender::Json
}
},
timeout_sec: spec.timeout_sec,
}
}
pub fn build_engine_arc(
hooks: &HooksConfig,
builtins: &BuiltinRegistry,
rt: &HookEngineCtx<'_>,
) -> Result<Arc<dyn defect_agent::hooks::HookEngine>, HookEngineBuildError> {
if hooks.is_empty() {
return Ok(Arc::new(defect_agent::hooks::NoopHookEngine));
}
let engine = build_hook_engine(hooks, builtins, rt)?;
Ok(Arc::new(engine))
}
pub fn build_main_session_engine(
hooks: &HooksConfig,
builtins: &BuiltinRegistry,
rt: &HookEngineCtx<'_>,
skills: &Arc<std::collections::BTreeMap<String, defect_agent::tool::SkillEntry>>,
goal: Option<&Arc<defect_agent::session::GoalState>>,
) -> Result<Arc<dyn defect_agent::hooks::HookEngine>, HookEngineBuildError> {
let mount_skills = !skills.is_empty();
if hooks.is_empty() && !mount_skills && goal.is_none() {
return Ok(Arc::new(defect_agent::hooks::NoopHookEngine));
}
let mut table = build_handler_table(hooks, builtins, rt)?;
if mount_skills {
use defect_agent::hooks::builtin::{SkillManifestHook, SkillTriggersHook};
table.push_step(
"after_session_enter",
StepHandlerEntry::new(
AgentHookMatcher::default(),
Arc::new(SkillManifestHook::new(skills.clone())),
)
.with_name(Some("skill-manifest".to_string())),
);
table.push_step(
"before_ingest",
StepHandlerEntry::new(
AgentHookMatcher::default(),
Arc::new(SkillTriggersHook::new(skills.clone())),
)
.with_name(Some("skill-triggers".to_string())),
);
}
if let Some(goal) = goal {
use defect_agent::hooks::builtin::GoalGate;
table.push_step(
"after_session_enter",
StepHandlerEntry::new(
AgentHookMatcher::default(),
Arc::new(GoalGate::new(goal.clone())),
)
.with_name(Some("goal-gate".to_string())),
);
table.push_step(
"before_turn_end",
StepHandlerEntry::new(
AgentHookMatcher::default(),
Arc::new(GoalGate::new(goal.clone())),
)
.with_name(Some("goal-gate".to_string())),
);
}
let engine = DefaultHookEngine::new();
engine.reload(table);
Ok(Arc::new(engine))
}
#[cfg(test)]
mod tests;