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),
AskDenied(String),
UpdatedInput(serde_json::Value),
}
#[derive(Debug, Clone)]
pub enum TurnDecision {
Continue,
ContinueWith(Vec<caliban_provider::Message>),
Stop,
}
#[derive(Debug, Clone, Default)]
pub struct SessionStartOutcome {
pub additional_context: Vec<String>,
}
#[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 session_id: &'a str,
pub turn_index: u32,
pub tool_use_id: &'a str,
pub tool_name: &'a str,
pub input: &'a serde_json::Value,
pub is_read_only: bool,
}
#[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<SessionStartOutcome> {
Ok(SessionStartOutcome::default())
}
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
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! forward_all_hooks_except {
( $inner:ident ; forward: $($method:ident),+ $(,)? ) => {
$( $crate::forward_all_hooks_except!(@one $inner $method); )+
};
( @one $inner:ident before_run ) => {
$crate::forward_all_hooks_except!(@fwd $inner before_run
<'l1,'l2>(ctx: &'l1 $crate::hooks::RunCtx<'l2>));
};
( @one $inner:ident after_run ) => {
$crate::forward_all_hooks_except!(@fwd $inner after_run
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::RunCtx<'l2>, outcome: &'l3 $crate::hooks::RunHookOutcome));
};
( @one $inner:ident after_run_failure ) => {
$crate::forward_all_hooks_except!(@fwd $inner after_run_failure
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::RunCtx<'l2>, outcome: &'l3 $crate::hooks::RunHookOutcome));
};
( @one $inner:ident before_turn ) => {
$crate::forward_all_hooks_except!(@fwd $inner before_turn
<'l1,'l2>(ctx: &'l1 $crate::hooks::TurnCtx<'l2>));
};
( @one $inner:ident after_turn ) => {
$crate::forward_all_hooks_except!(@ret $inner after_turn $crate::hooks::TurnDecision;
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::TurnCtx<'l2>, outcome: &'l3 $crate::TurnOutcome));
};
( @one $inner:ident after_turn_failure ) => {
$crate::forward_all_hooks_except!(@fwd $inner after_turn_failure
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::TurnCtx<'l2>, outcome: &'l3 $crate::TurnOutcome));
};
( @one $inner:ident before_tool ) => {
$crate::forward_all_hooks_except!(@ret $inner before_tool $crate::hooks::HookDecision;
<'l1,'l2>(ctx: &'l1 $crate::hooks::ToolCtx<'l2>));
};
( @one $inner:ident after_tool ) => {
$crate::forward_all_hooks_except!(@fwd $inner after_tool
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::ToolCtx<'l2>, result: &'l3 ::std::result::Result<::std::vec::Vec<$crate::ContentBlock>, $crate::tool::ToolError>));
};
( @one $inner:ident session_start ) => {
$crate::forward_all_hooks_except!(@ret $inner session_start $crate::hooks::SessionStartOutcome;
<'l1,'l2>(ctx: &'l1 $crate::hooks::SessionCtx<'l2>));
};
( @one $inner:ident session_end ) => {
$crate::forward_all_hooks_except!(@fwd $inner session_end
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::SessionCtx<'l2>, outcome: &'l3 $crate::hooks::SessionOutcome));
};
( @one $inner:ident user_prompt_submit ) => {
$crate::forward_all_hooks_except!(@ret $inner user_prompt_submit $crate::hooks::HookDecision;
<'l1,'l2>(ctx: &'l1 $crate::hooks::PromptCtx<'l2>));
};
( @one $inner:ident pre_compact ) => {
$crate::forward_all_hooks_except!(@fwd $inner pre_compact
<'l1,'l2>(ctx: &'l1 $crate::hooks::CompactCtx<'l2>));
};
( @one $inner:ident post_compact ) => {
$crate::forward_all_hooks_except!(@fwd $inner post_compact
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::CompactCtx<'l2>, outcome: &'l3 $crate::hooks::CompactOutcome));
};
( @one $inner:ident config_change ) => {
$crate::forward_all_hooks_except!(@fwd $inner config_change
<'l1,'l2>(ctx: &'l1 $crate::hooks::ConfigChangeCtx<'l2>));
};
( @one $inner:ident cwd_changed ) => {
$crate::forward_all_hooks_except!(@fwd $inner cwd_changed
<'l1,'l2>(ctx: &'l1 $crate::hooks::CwdChangedCtx<'l2>));
};
( @one $inner:ident file_changed ) => {
$crate::forward_all_hooks_except!(@fwd $inner file_changed
<'l1,'l2>(ctx: &'l1 $crate::hooks::FileChangedCtx<'l2>));
};
( @one $inner:ident permission_request ) => {
$crate::forward_all_hooks_except!(@fwd $inner permission_request
<'l1,'l2>(ctx: &'l1 $crate::hooks::PermCtx<'l2>));
};
( @one $inner:ident permission_denied ) => {
$crate::forward_all_hooks_except!(@fwd $inner permission_denied
<'l1,'l2>(ctx: &'l1 $crate::hooks::PermCtx<'l2>));
};
( @one $inner:ident notification ) => {
$crate::forward_all_hooks_except!(@fwd $inner notification
<'l1,'l2>(ctx: &'l1 $crate::hooks::NotificationCtx<'l2>));
};
( @one $inner:ident subagent_start ) => {
$crate::forward_all_hooks_except!(@fwd $inner subagent_start
<'l1,'l2>(ctx: &'l1 $crate::hooks::SubagentCtx<'l2>));
};
( @one $inner:ident subagent_stop ) => {
$crate::forward_all_hooks_except!(@fwd $inner subagent_stop
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::SubagentCtx<'l2>, outcome: &'l3 $crate::hooks::SubagentOutcome));
};
( @one $inner:ident task_created ) => {
$crate::forward_all_hooks_except!(@fwd $inner task_created
<'l1,'l2>(ctx: &'l1 $crate::hooks::TaskCtx<'l2>));
};
( @one $inner:ident task_completed ) => {
$crate::forward_all_hooks_except!(@fwd $inner task_completed
<'l1,'l2,'l3>(ctx: &'l1 $crate::hooks::TaskCtx<'l2>, outcome: &'l3 $crate::hooks::TaskOutcome));
};
( @fwd $inner:ident $method:ident
< $($life:lifetime),* > ( $($arg:ident : $ty:ty),* $(,)? )
) => {
fn $method<'life0, $($life,)* 'async_trait>(
&'life0 self,
$($arg: $ty),*
) -> ::core::pin::Pin<Box<
dyn ::core::future::Future<Output = $crate::Result<()>>
+ ::core::marker::Send + 'async_trait,
>>
where
'life0: 'async_trait,
$($life: 'async_trait,)*
Self: 'async_trait,
{
Box::pin(async move {
$crate::hooks::Hooks::$method(&*self.$inner, $($arg),*).await
})
}
};
( @ret $inner:ident $method:ident $ret:ty;
< $($life:lifetime),* > ( $($arg:ident : $ty:ty),* $(,)? )
) => {
fn $method<'life0, $($life,)* 'async_trait>(
&'life0 self,
$($arg: $ty),*
) -> ::core::pin::Pin<Box<
dyn ::core::future::Future<Output = $crate::Result<$ret>>
+ ::core::marker::Send + 'async_trait,
>>
where
'life0: 'async_trait,
$($life: 'async_trait,)*
Self: 'async_trait,
{
Box::pin(async move {
$crate::hooks::Hooks::$method(&*self.$inner, $($arg),*).await
})
}
};
}
#[async_trait]
pub trait ToolGate: Send + Sync {
async fn before_tool(&self, ctx: &ToolCtx<'_>) -> Result<HookDecision>;
async fn after_tool(
&self,
ctx: &ToolCtx<'_>,
result: &std::result::Result<Vec<ContentBlock>, ToolError>,
) -> Result<()>;
}
#[async_trait]
impl<T: Hooks + ?Sized> ToolGate for T {
async fn before_tool(&self, ctx: &ToolCtx<'_>) -> Result<HookDecision> {
Hooks::before_tool(self, ctx).await
}
async fn after_tool(
&self,
ctx: &ToolCtx<'_>,
result: &std::result::Result<Vec<ContentBlock>, ToolError>,
) -> Result<()> {
Hooks::after_tool(self, ctx, result).await
}
}
#[async_trait]
pub trait TurnObserver: Send + Sync {
async fn before_turn(&self, ctx: &TurnCtx<'_>) -> Result<()>;
async fn after_turn(
&self,
ctx: &TurnCtx<'_>,
outcome: &crate::TurnOutcome,
) -> Result<TurnDecision>;
async fn after_turn_failure(
&self,
ctx: &TurnCtx<'_>,
outcome: &crate::TurnOutcome,
) -> Result<()>;
}
#[async_trait]
impl<T: Hooks + ?Sized> TurnObserver for T {
async fn before_turn(&self, ctx: &TurnCtx<'_>) -> Result<()> {
Hooks::before_turn(self, ctx).await
}
async fn after_turn(
&self,
ctx: &TurnCtx<'_>,
outcome: &crate::TurnOutcome,
) -> Result<TurnDecision> {
Hooks::after_turn(self, ctx, outcome).await
}
async fn after_turn_failure(
&self,
ctx: &TurnCtx<'_>,
outcome: &crate::TurnOutcome,
) -> Result<()> {
Hooks::after_turn_failure(self, ctx, outcome).await
}
}
#[async_trait]
pub trait LifecycleObserver: Send + Sync {
async fn before_run(&self, ctx: &RunCtx<'_>) -> Result<()>;
async fn after_run(&self, ctx: &RunCtx<'_>, outcome: &RunHookOutcome) -> Result<()>;
async fn after_run_failure(&self, ctx: &RunCtx<'_>, outcome: &RunHookOutcome) -> Result<()>;
async fn session_start(&self, ctx: &SessionCtx<'_>) -> Result<SessionStartOutcome>;
async fn session_end(&self, ctx: &SessionCtx<'_>, outcome: &SessionOutcome) -> Result<()>;
}
#[async_trait]
impl<T: Hooks + ?Sized> LifecycleObserver for T {
async fn before_run(&self, ctx: &RunCtx<'_>) -> Result<()> {
Hooks::before_run(self, ctx).await
}
async fn after_run(&self, ctx: &RunCtx<'_>, outcome: &RunHookOutcome) -> Result<()> {
Hooks::after_run(self, ctx, outcome).await
}
async fn after_run_failure(&self, ctx: &RunCtx<'_>, outcome: &RunHookOutcome) -> Result<()> {
Hooks::after_run_failure(self, ctx, outcome).await
}
async fn session_start(&self, ctx: &SessionCtx<'_>) -> Result<SessionStartOutcome> {
Hooks::session_start(self, ctx).await
}
async fn session_end(&self, ctx: &SessionCtx<'_>, outcome: &SessionOutcome) -> Result<()> {
Hooks::session_end(self, ctx, outcome).await
}
}
#[async_trait]
pub trait PromptGate: Send + Sync {
async fn user_prompt_submit(&self, ctx: &PromptCtx<'_>) -> Result<HookDecision>;
}
#[async_trait]
impl<T: Hooks + ?Sized> PromptGate for T {
async fn user_prompt_submit(&self, ctx: &PromptCtx<'_>) -> Result<HookDecision> {
Hooks::user_prompt_submit(self, ctx).await
}
}
#[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
}
}
macro_rules! composite_fanout_fwd {
(
$method:ident < $($life:lifetime),* >
( $($arg:ident : $ty:ty),* $(,)? )
) => {
fn $method<'life0, $($life,)* 'async_trait>(
&'life0 self,
$($arg: $ty),*
) -> ::core::pin::Pin<Box<
dyn ::core::future::Future<Output = Result<()>> + ::core::marker::Send + 'async_trait,
>>
where
'life0: 'async_trait,
$($life: 'async_trait,)*
Self: 'async_trait,
{
Box::pin(async move {
if self.all_noop {
return Ok(());
}
for h in &self.layers {
h.$method($($arg),*).await?;
}
Ok(())
})
}
};
}
macro_rules! composite_fanout_rev {
(
$method:ident < $($life:lifetime),* >
( $($arg:ident : $ty:ty),* $(,)? )
) => {
fn $method<'life0, $($life,)* 'async_trait>(
&'life0 self,
$($arg: $ty),*
) -> ::core::pin::Pin<Box<
dyn ::core::future::Future<Output = Result<()>> + ::core::marker::Send + 'async_trait,
>>
where
'life0: 'async_trait,
$($life: 'async_trait,)*
Self: 'async_trait,
{
Box::pin(async move {
if self.all_noop {
return Ok(());
}
for h in self.layers.iter().rev() {
h.$method($($arg),*).await?;
}
Ok(())
})
}
};
}
#[async_trait]
impl Hooks for CompositeHooks {
composite_fanout_fwd!(before_run<'l1, 'l2>(ctx: &'l1 RunCtx<'l2>));
composite_fanout_rev!(
after_run<'l1, 'l2, 'l3>(ctx: &'l1 RunCtx<'l2>, outcome: &'l3 RunHookOutcome)
);
composite_fanout_rev!(
after_run_failure<'l1, 'l2, 'l3>(ctx: &'l1 RunCtx<'l2>, outcome: &'l3 RunHookOutcome)
);
composite_fanout_fwd!(before_turn<'l1, 'l2>(ctx: &'l1 TurnCtx<'l2>));
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)
}
composite_fanout_rev!(
after_turn_failure<'l1, 'l2, 'l3>(ctx: &'l1 TurnCtx<'l2>, outcome: &'l3 crate::TurnOutcome)
);
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 {
session_id: ctx.session_id,
turn_index: ctx.turn_index,
tool_use_id: ctx.tool_use_id,
tool_name: ctx.tool_name,
input: effective_input,
is_read_only: ctx.is_read_only,
};
match h.before_tool(&layer_ctx).await? {
HookDecision::Allow => {}
HookDecision::Deny(msg) => return Ok(HookDecision::Deny(msg)),
HookDecision::AskDenied(msg) => return Ok(HookDecision::AskDenied(msg)),
HookDecision::UpdatedInput(new_input) => {
latest_input = Some(new_input);
}
}
}
match latest_input {
Some(v) => Ok(HookDecision::UpdatedInput(v)),
None => Ok(HookDecision::Allow),
}
}
composite_fanout_rev!(after_tool<'l1, 'l2, 'l3>(
ctx: &'l1 ToolCtx<'l2>,
result: &'l3 std::result::Result<Vec<ContentBlock>, ToolError>,
));
async fn session_start(&self, ctx: &SessionCtx<'_>) -> Result<SessionStartOutcome> {
if self.all_noop {
return Ok(SessionStartOutcome::default());
}
let mut merged = SessionStartOutcome::default();
for h in &self.layers {
let outcome = h.session_start(ctx).await?;
merged.additional_context.extend(outcome.additional_context);
}
Ok(merged)
}
composite_fanout_rev!(
session_end<'l1, 'l2, 'l3>(ctx: &'l1 SessionCtx<'l2>, outcome: &'l3 SessionOutcome)
);
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::AskDenied(msg) => return Ok(HookDecision::AskDenied(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),
}
}
composite_fanout_fwd!(pre_compact<'l1, 'l2>(ctx: &'l1 CompactCtx<'l2>));
composite_fanout_rev!(
post_compact<'l1, 'l2, 'l3>(ctx: &'l1 CompactCtx<'l2>, outcome: &'l3 CompactOutcome)
);
composite_fanout_fwd!(config_change<'l1, 'l2>(ctx: &'l1 ConfigChangeCtx<'l2>));
composite_fanout_fwd!(cwd_changed<'l1, 'l2>(ctx: &'l1 CwdChangedCtx<'l2>));
composite_fanout_fwd!(file_changed<'l1, 'l2>(ctx: &'l1 FileChangedCtx<'l2>));
composite_fanout_fwd!(permission_request<'l1, 'l2>(ctx: &'l1 PermCtx<'l2>));
composite_fanout_fwd!(permission_denied<'l1, 'l2>(ctx: &'l1 PermCtx<'l2>));
composite_fanout_fwd!(notification<'l1, 'l2>(ctx: &'l1 NotificationCtx<'l2>));
composite_fanout_fwd!(subagent_start<'l1, 'l2>(ctx: &'l1 SubagentCtx<'l2>));
composite_fanout_rev!(
subagent_stop<'l1, 'l2, 'l3>(ctx: &'l1 SubagentCtx<'l2>, outcome: &'l3 SubagentOutcome)
);
composite_fanout_fwd!(task_created<'l1, 'l2>(ctx: &'l1 TaskCtx<'l2>));
composite_fanout_rev!(
task_completed<'l1, 'l2, 'l3>(ctx: &'l1 TaskCtx<'l2>, outcome: &'l3 TaskOutcome)
);
}
#[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 pre_tool_envelope(event: &str, ctx: &ToolCtx<'_>) -> serde_json::Value {
build_envelope(
event,
serde_json::json!({
"session_id": ctx.session_id,
"turn_index": ctx.turn_index,
"tool": {
"name": ctx.tool_name,
"useId": ctx.tool_use_id,
"input": ctx.input,
}
}),
)
}
#[must_use]
pub fn session_start_envelope(ctx: &SessionCtx<'_>) -> serde_json::Value {
build_envelope(
"SessionStart",
serde_json::json!({
"session_id": ctx.session_id,
"cwd": ctx.cwd.display().to_string(),
"provider": ctx.provider,
"model": ctx.model,
}),
)
}
#[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
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn composite_session_start_concatenates_context_in_order() {
struct CtxHook(&'static str);
#[async_trait]
impl Hooks for CtxHook {
async fn session_start(&self, _ctx: &SessionCtx<'_>) -> Result<SessionStartOutcome> {
Ok(SessionStartOutcome {
additional_context: vec![self.0.to_string()],
})
}
}
let composite = CompositeHooks::new(vec![
std::sync::Arc::new(CtxHook("first")) as std::sync::Arc<dyn Hooks>,
std::sync::Arc::new(CtxHook("second")) as std::sync::Arc<dyn Hooks>,
]);
let cwd = std::path::Path::new(".");
let ctx = SessionCtx {
session_id: "t",
cwd,
provider: "test",
model: "m",
};
let out = Hooks::session_start(&composite, &ctx).await.unwrap();
assert_eq!(
out.additional_context,
vec!["first".to_string(), "second".to_string()]
);
}
}