use std::path::{Path, PathBuf};
use async_trait::async_trait;
use caliban_provider::{ContentBlock, Message};
use tokio_util::sync::CancellationToken;
use crate::AgentConfig;
use crate::error::Result;
use crate::tool::ToolError;
#[derive(Debug, Clone)]
pub enum HookDecision {
Allow,
Deny(String),
UpdatedInput(serde_json::Value),
}
#[derive(Debug, Clone)]
pub enum TurnDecision {
Continue,
ContinueWith(Vec<caliban_provider::Message>),
Stop,
}
#[derive(Debug)]
pub struct RunCtx<'a> {
pub session_id: &'a str,
pub workspace_root: &'a Path,
pub user_message: Option<&'a Message>,
pub prompt_index: u32,
pub cancel: CancellationToken,
}
#[derive(Debug, Clone)]
pub struct RunHookOutcome {
pub turn_count: u32,
pub input_tokens: u32,
pub output_tokens: u32,
pub success: bool,
}
#[derive(Debug)]
pub struct TurnCtx<'a> {
pub turn_index: u32,
pub messages: &'a [Message],
pub config: &'a AgentConfig,
}
#[derive(Debug)]
pub struct ToolCtx<'a> {
pub turn_index: u32,
pub tool_use_id: &'a str,
pub tool_name: &'a str,
pub input: &'a serde_json::Value,
}
#[derive(Debug)]
pub struct SessionCtx<'a> {
pub session_id: &'a str,
pub cwd: &'a Path,
pub provider: &'a str,
pub model: &'a str,
}
#[derive(Debug, Clone)]
pub struct SessionOutcome {
pub turn_count: u32,
pub input_tokens: u32,
pub output_tokens: u32,
}
#[derive(Debug)]
pub struct PromptCtx<'a> {
pub session_id: &'a str,
pub cwd: &'a Path,
pub turn_index: u32,
pub prompt: &'a str,
pub attachments: &'a [String],
}
#[derive(Debug)]
pub struct CompactCtx<'a> {
pub session_id: &'a str,
pub token_count_before: u32,
pub strategy: &'a str,
}
#[derive(Debug, Clone)]
pub struct CompactOutcome {
pub token_count_after: u32,
pub compacted: bool,
}
#[derive(Debug)]
pub struct ConfigChangeCtx<'a> {
pub changed_keys: &'a [String],
pub new_settings_summary: &'a str,
}
#[derive(Debug)]
pub struct CwdChangedCtx<'a> {
pub old_cwd: &'a Path,
pub new_cwd: &'a Path,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileChangeKind {
Created,
Modified,
Deleted,
}
impl FileChangeKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Created => "created",
Self::Modified => "modified",
Self::Deleted => "deleted",
}
}
}
#[derive(Debug)]
pub struct FileChangedCtx<'a> {
pub path: &'a Path,
pub kind: FileChangeKind,
pub tool: &'a str,
}
#[derive(Debug)]
pub struct PermCtx<'a> {
pub turn_index: u32,
pub tool_use_id: &'a str,
pub tool_name: &'a str,
pub input: &'a serde_json::Value,
pub rule_action: &'a str,
pub rule_comment: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotificationLevel {
Info,
Warn,
Error,
}
impl NotificationLevel {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
}
}
}
#[derive(Debug)]
pub struct NotificationCtx<'a> {
pub level: NotificationLevel,
pub message: &'a str,
}
#[derive(Debug)]
pub struct SubagentCtx<'a> {
pub parent_turn_index: u32,
pub agent_name: &'a str,
pub task_id: &'a str,
}
#[derive(Debug, Clone)]
pub struct SubagentOutcome {
pub success: bool,
pub final_text: String,
}
#[derive(Debug)]
pub struct TaskCtx<'a> {
pub task_id: &'a str,
pub content: &'a str,
pub status: &'a str,
}
#[derive(Debug, Clone)]
pub struct TaskOutcome {
pub terminal_status: String,
}
#[async_trait]
pub trait Hooks: Send + Sync {
async fn before_run(&self, _ctx: &RunCtx<'_>) -> Result<()> {
Ok(())
}
async fn after_run(&self, _ctx: &RunCtx<'_>, _outcome: &RunHookOutcome) -> Result<()> {
Ok(())
}
async fn after_run_failure(&self, _ctx: &RunCtx<'_>, _outcome: &RunHookOutcome) -> Result<()> {
Ok(())
}
async fn before_turn(&self, _ctx: &TurnCtx<'_>) -> Result<()> {
Ok(())
}
async fn after_turn(
&self,
_ctx: &TurnCtx<'_>,
_outcome: &crate::TurnOutcome,
) -> Result<TurnDecision> {
Ok(TurnDecision::Continue)
}
async fn after_turn_failure(
&self,
_ctx: &TurnCtx<'_>,
_outcome: &crate::TurnOutcome,
) -> Result<()> {
Ok(())
}
async fn before_tool(&self, _ctx: &ToolCtx<'_>) -> Result<HookDecision> {
Ok(HookDecision::Allow)
}
async fn after_tool(
&self,
_ctx: &ToolCtx<'_>,
_result: &std::result::Result<Vec<ContentBlock>, ToolError>,
) -> Result<()> {
Ok(())
}
async fn session_start(&self, _ctx: &SessionCtx<'_>) -> Result<()> {
Ok(())
}
async fn session_end(&self, _ctx: &SessionCtx<'_>, _outcome: &SessionOutcome) -> Result<()> {
Ok(())
}
async fn user_prompt_submit(&self, _ctx: &PromptCtx<'_>) -> Result<HookDecision> {
Ok(HookDecision::Allow)
}
async fn pre_compact(&self, _ctx: &CompactCtx<'_>) -> Result<()> {
Ok(())
}
async fn post_compact(&self, _ctx: &CompactCtx<'_>, _outcome: &CompactOutcome) -> Result<()> {
Ok(())
}
async fn config_change(&self, _ctx: &ConfigChangeCtx<'_>) -> Result<()> {
Ok(())
}
async fn cwd_changed(&self, _ctx: &CwdChangedCtx<'_>) -> Result<()> {
Ok(())
}
async fn file_changed(&self, _ctx: &FileChangedCtx<'_>) -> Result<()> {
Ok(())
}
async fn permission_request(&self, _ctx: &PermCtx<'_>) -> Result<()> {
Ok(())
}
async fn permission_denied(&self, _ctx: &PermCtx<'_>) -> Result<()> {
Ok(())
}
async fn notification(&self, _ctx: &NotificationCtx<'_>) -> Result<()> {
Ok(())
}
async fn subagent_start(&self, _ctx: &SubagentCtx<'_>) -> Result<()> {
Ok(())
}
async fn subagent_stop(
&self,
_ctx: &SubagentCtx<'_>,
_outcome: &SubagentOutcome,
) -> Result<()> {
Ok(())
}
async fn task_created(&self, _ctx: &TaskCtx<'_>) -> Result<()> {
Ok(())
}
async fn task_completed(&self, _ctx: &TaskCtx<'_>, _outcome: &TaskOutcome) -> Result<()> {
Ok(())
}
fn is_noop(&self) -> bool {
false
}
}
#[derive(Debug, Default)]
pub struct NoopHooks;
#[async_trait]
impl Hooks for NoopHooks {
fn is_noop(&self) -> bool {
true
}
}
pub struct CompositeHooks {
layers: Vec<std::sync::Arc<dyn Hooks>>,
all_noop: bool,
}
impl std::fmt::Debug for CompositeHooks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompositeHooks")
.field("layers", &self.layers.len())
.field("all_noop", &self.all_noop)
.finish()
}
}
impl CompositeHooks {
#[must_use]
pub fn new(layers: Vec<std::sync::Arc<dyn Hooks>>) -> Self {
let all_noop = layers.iter().all(|h| h.is_noop());
Self { layers, all_noop }
}
pub fn push(&mut self, layer: std::sync::Arc<dyn Hooks>) {
if !layer.is_noop() {
self.all_noop = false;
}
self.layers.push(layer);
}
#[must_use]
pub fn len(&self) -> usize {
self.layers.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.layers.is_empty()
}
#[must_use]
pub fn all_noop(&self) -> bool {
self.all_noop
}
}
#[async_trait]
impl Hooks for CompositeHooks {
async fn before_run(&self, ctx: &RunCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.before_run(ctx).await?;
}
Ok(())
}
async fn after_run(&self, ctx: &RunCtx<'_>, outcome: &RunHookOutcome) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.after_run(ctx, outcome).await?;
}
Ok(())
}
async fn after_run_failure(&self, ctx: &RunCtx<'_>, outcome: &RunHookOutcome) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.after_run_failure(ctx, outcome).await?;
}
Ok(())
}
async fn before_turn(&self, ctx: &TurnCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.before_turn(ctx).await?;
}
Ok(())
}
async fn after_turn(
&self,
ctx: &TurnCtx<'_>,
outcome: &crate::TurnOutcome,
) -> Result<TurnDecision> {
if self.all_noop {
return Ok(TurnDecision::Continue);
}
let mut latest = TurnDecision::Continue;
for h in self.layers.iter().rev() {
match h.after_turn(ctx, outcome).await? {
TurnDecision::Continue => {}
d @ (TurnDecision::ContinueWith(_) | TurnDecision::Stop) => {
latest = d;
break;
}
}
}
Ok(latest)
}
async fn after_turn_failure(
&self,
ctx: &TurnCtx<'_>,
outcome: &crate::TurnOutcome,
) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.after_turn_failure(ctx, outcome).await?;
}
Ok(())
}
async fn before_tool(&self, ctx: &ToolCtx<'_>) -> Result<HookDecision> {
if self.all_noop {
return Ok(HookDecision::Allow);
}
let mut latest_input: Option<serde_json::Value> = None;
for h in &self.layers {
let effective_input: &serde_json::Value = latest_input.as_ref().unwrap_or(ctx.input);
let layer_ctx = ToolCtx {
turn_index: ctx.turn_index,
tool_use_id: ctx.tool_use_id,
tool_name: ctx.tool_name,
input: effective_input,
};
match h.before_tool(&layer_ctx).await? {
HookDecision::Allow => {}
HookDecision::Deny(msg) => return Ok(HookDecision::Deny(msg)),
HookDecision::UpdatedInput(new_input) => {
latest_input = Some(new_input);
}
}
}
match latest_input {
Some(v) => Ok(HookDecision::UpdatedInput(v)),
None => Ok(HookDecision::Allow),
}
}
async fn after_tool(
&self,
ctx: &ToolCtx<'_>,
result: &std::result::Result<Vec<ContentBlock>, ToolError>,
) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.after_tool(ctx, result).await?;
}
Ok(())
}
async fn session_start(&self, ctx: &SessionCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.session_start(ctx).await?;
}
Ok(())
}
async fn session_end(&self, ctx: &SessionCtx<'_>, outcome: &SessionOutcome) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.session_end(ctx, outcome).await?;
}
Ok(())
}
async fn user_prompt_submit(&self, ctx: &PromptCtx<'_>) -> Result<HookDecision> {
if self.all_noop {
return Ok(HookDecision::Allow);
}
let mut latest_prompt: Option<String> = None;
for h in &self.layers {
let effective_prompt = latest_prompt.as_deref().unwrap_or(ctx.prompt);
let layer_ctx = PromptCtx {
session_id: ctx.session_id,
cwd: ctx.cwd,
turn_index: ctx.turn_index,
prompt: effective_prompt,
attachments: ctx.attachments,
};
match h.user_prompt_submit(&layer_ctx).await? {
HookDecision::Allow => {}
HookDecision::Deny(msg) => return Ok(HookDecision::Deny(msg)),
HookDecision::UpdatedInput(new_input) => {
if let Some(s) = new_input.as_str() {
latest_prompt = Some(s.to_string());
} else {
latest_prompt = Some(new_input.to_string());
}
}
}
}
match latest_prompt {
Some(s) => Ok(HookDecision::UpdatedInput(serde_json::Value::String(s))),
None => Ok(HookDecision::Allow),
}
}
async fn pre_compact(&self, ctx: &CompactCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.pre_compact(ctx).await?;
}
Ok(())
}
async fn post_compact(&self, ctx: &CompactCtx<'_>, outcome: &CompactOutcome) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.post_compact(ctx, outcome).await?;
}
Ok(())
}
async fn config_change(&self, ctx: &ConfigChangeCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.config_change(ctx).await?;
}
Ok(())
}
async fn cwd_changed(&self, ctx: &CwdChangedCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.cwd_changed(ctx).await?;
}
Ok(())
}
async fn file_changed(&self, ctx: &FileChangedCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.file_changed(ctx).await?;
}
Ok(())
}
async fn permission_request(&self, ctx: &PermCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.permission_request(ctx).await?;
}
Ok(())
}
async fn permission_denied(&self, ctx: &PermCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.permission_denied(ctx).await?;
}
Ok(())
}
async fn notification(&self, ctx: &NotificationCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.notification(ctx).await?;
}
Ok(())
}
async fn subagent_start(&self, ctx: &SubagentCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.subagent_start(ctx).await?;
}
Ok(())
}
async fn subagent_stop(&self, ctx: &SubagentCtx<'_>, outcome: &SubagentOutcome) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.subagent_stop(ctx, outcome).await?;
}
Ok(())
}
async fn task_created(&self, ctx: &TaskCtx<'_>) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.task_created(ctx).await?;
}
Ok(())
}
async fn task_completed(&self, ctx: &TaskCtx<'_>, outcome: &TaskOutcome) -> Result<()> {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.task_completed(ctx, outcome).await?;
}
Ok(())
}
}
#[must_use]
pub fn build_envelope(event_name: &str, fields: serde_json::Value) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert(
"hookEventName".into(),
serde_json::Value::String(event_name.to_string()),
);
if let serde_json::Value::Object(map) = fields {
for (k, v) in map {
obj.insert(k, v);
}
}
serde_json::Value::Object(obj)
}
#[must_use]
pub fn envelope_with_cwd(
event_name: &str,
cwd: &Path,
mut fields: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
fields.insert(
"cwd".into(),
serde_json::Value::String(cwd.display().to_string()),
);
build_envelope(event_name, serde_json::Value::Object(fields))
}
#[allow(dead_code)]
fn path_to_value(p: &Path) -> serde_json::Value {
serde_json::Value::String(p.display().to_string())
}
#[doc(hidden)]
pub fn __noop_path_use(p: PathBuf) -> PathBuf {
p
}