Skip to main content

open_agent/hooks/
registry.rs

1/// An ordered registry of asynchronous lifecycle hooks.
2///
3/// The first handler returning `Some(HookDecision)` determines the result and
4/// skips later handlers. Return `None` to let subsequent handlers run. Clones share
5/// handlers through [`Arc`]; the three vectors also permit direct registration.
6///
7/// ```rust
8/// use open_agent::{Hooks, HookDecision};
9///
10/// let hooks = Hooks::new()
11///     .add_pre_tool_use(|event| async move {
12///         if event.tool_name == "delete_file" {
13///             return Some(HookDecision::block("Deletion is disabled"));
14///         }
15///         None
16///     })
17///     .add_post_tool_use(|event| async move {
18///         println!("Completed {}", event.tool_name);
19///         None
20///     })
21///     .add_user_prompt_submit(|event| async move {
22///         let prompt = format!("{}\nAnswer in JSON.", event.prompt);
23///         Some(HookDecision::modify_prompt(prompt, "Requested output format"))
24///     });
25/// ```
26#[derive(Clone, Default)]
27pub struct Hooks {
28    /// Collection of PreToolUse hook handlers, executed in registration order
29    pub pre_tool_use: Vec<PreToolUseHandler>,
30
31    /// Collection of PostToolUse hook handlers, executed in registration order
32    pub post_tool_use: Vec<PostToolUseHandler>,
33
34    /// Collection of UserPromptSubmit hook handlers, executed in registration order
35    pub user_prompt_submit: Vec<UserPromptSubmitHandler>,
36}
37
38impl Hooks {
39    /// Creates an empty hook registry.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Appends a pre-tool handler. See [`Hooks`] for ordering and an example.
45    pub fn add_pre_tool_use<F, Fut>(mut self, handler: F) -> Self
46    where
47        F: Fn(PreToolUseEvent) -> Fut + Send + Sync + 'static,
48        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
49    {
50        self.pre_tool_use
51            .push(Arc::new(move |event| Box::pin(handler(event))));
52        self
53    }
54
55    /// Appends a post-tool handler. Return `None` from observers so later hooks run.
56    pub fn add_post_tool_use<F, Fut>(mut self, handler: F) -> Self
57    where
58        F: Fn(PostToolUseEvent) -> Fut + Send + Sync + 'static,
59        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
60    {
61        self.post_tool_use
62            .push(Arc::new(move |event| Box::pin(handler(event))));
63        self
64    }
65
66    /// Appends a prompt handler. Return a decision to block or replace the prompt.
67    pub fn add_user_prompt_submit<F, Fut>(mut self, handler: F) -> Self
68    where
69        F: Fn(UserPromptSubmitEvent) -> Fut + Send + Sync + 'static,
70        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
71    {
72        self.user_prompt_submit
73            .push(Arc::new(move |event| Box::pin(handler(event))));
74        self
75    }
76
77    /// Runs handlers in registration order, returning the first `Some` decision.
78    ///
79    /// Each handler receives the same event snapshot. Returns `None` if no handler
80    /// decides, including when the registry is empty.
81    pub async fn execute_pre_tool_use(&self, event: PreToolUseEvent) -> Option<HookDecision> {
82        let (last, preceding) = self.pre_tool_use.split_last()?;
83        for handler in preceding {
84            let decision = handler(event.clone()).await;
85            if decision.is_some() {
86                return decision;
87            }
88        }
89        last(event).await
90    }
91
92    /// Runs post-tool handlers with the same first-decision behavior as
93    /// [`execute_pre_tool_use`](Self::execute_pre_tool_use).
94    pub async fn execute_post_tool_use(&self, event: PostToolUseEvent) -> Option<HookDecision> {
95        let (last, preceding) = self.post_tool_use.split_last()?;
96        for handler in preceding {
97            let decision = handler(event.clone()).await;
98            if decision.is_some() {
99                return decision;
100            }
101        }
102        last(event).await
103    }
104
105    /// Executes all registered UserPromptSubmit hooks in order and returns the first decision.
106    ///
107    /// Identical in behavior to [`Self::execute_pre_tool_use`] but for UserPromptSubmit events.
108    /// See that method for detailed documentation of the execution model.
109    pub async fn execute_user_prompt_submit(
110        &self,
111        event: UserPromptSubmitEvent,
112    ) -> Option<HookDecision> {
113        let (last, preceding) = self.user_prompt_submit.split_last()?;
114        for handler in preceding {
115            let decision = handler(event.clone()).await;
116            if decision.is_some() {
117                return decision;
118            }
119        }
120        last(event).await
121    }
122}
123
124/// Shows handler counts, omitting the closures.
125impl std::fmt::Debug for Hooks {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("Hooks")
128            .field(
129                "pre_tool_use",
130                &format!("{} handlers", self.pre_tool_use.len()),
131            )
132            .field(
133                "post_tool_use",
134                &format!("{} handlers", self.post_tool_use.len()),
135            )
136            .field(
137                "user_prompt_submit",
138                &format!("{} handlers", self.user_prompt_submit.len()),
139            )
140            .finish()
141    }
142}
143
144/// String constant for the PreToolUse hook event name.
145///
146/// This constant can be used for logging, metrics, or when you need a string
147/// representation of the hook type. It's primarily used internally but is exposed
148/// as part of the public API for consistency.
149pub const HOOK_PRE_TOOL_USE: &str = "pre_tool_use";
150
151/// String constant for the PostToolUse hook event name.
152///
153/// See [`HOOK_PRE_TOOL_USE`] for usage details.
154pub const HOOK_POST_TOOL_USE: &str = "post_tool_use";
155
156/// String constant for the UserPromptSubmit hook event name.
157///
158/// See [`HOOK_PRE_TOOL_USE`] for usage details.
159pub const HOOK_USER_PROMPT_SUBMIT: &str = "user_prompt_submit";