Skip to main content

open_agent/hooks/
registry.rs

1/// Container for registering and managing lifecycle hooks.
2///
3/// The `Hooks` struct stores collections of hook handlers for different lifecycle events.
4/// It provides a builder pattern for registering hooks and executor methods for running them.
5///
6/// # Design Principles
7///
8/// - **Builder Pattern**: Hooks can be chained during construction using `.add_*()` methods
9/// - **Multiple Hooks**: You can register multiple hooks for the same event type
10/// - **Execution Order**: Hooks execute in the order they were registered (FIFO)
11/// - **First Wins**: The first hook returning `Some(HookDecision)` determines the outcome
12/// - **Thread Safe**: The struct is `Clone` and all handlers are `Arc`-wrapped for sharing
13///
14/// # Example: Building a Hooks Collection
15///
16/// ```rust
17/// use open_agent::{Hooks, PreToolUseEvent, PostToolUseEvent, HookDecision};
18///
19/// let hooks = Hooks::new()
20///     // First: Security gate (highest priority)
21///     .add_pre_tool_use(|event| async move {
22///         if event.tool_name == "dangerous" {
23///             return Some(HookDecision::block("Security violation"));
24///         }
25///         None
26///     })
27///     // Second: Rate limiting
28///     .add_pre_tool_use(|event| async move {
29///         // Check rate limits...
30///         None
31///     })
32///     // Audit logging (happens after execution)
33///     .add_post_tool_use(|event| async move {
34///         println!("Tool '{}' executed", event.tool_name);
35///         None
36///     });
37/// ```
38///
39/// # Fields
40///
41/// - `pre_tool_use`: Handlers invoked before tool execution
42/// - `post_tool_use`: Handlers invoked after tool execution
43/// - `user_prompt_submit`: Handlers invoked before processing user prompts
44///
45/// All fields are public, allowing direct manipulation if needed, though the builder
46/// methods are the recommended approach.
47#[derive(Clone, Default)]
48pub struct Hooks {
49    /// Collection of PreToolUse hook handlers, executed in registration order
50    pub pre_tool_use: Vec<PreToolUseHandler>,
51
52    /// Collection of PostToolUse hook handlers, executed in registration order
53    pub post_tool_use: Vec<PostToolUseHandler>,
54
55    /// Collection of UserPromptSubmit hook handlers, executed in registration order
56    pub user_prompt_submit: Vec<UserPromptSubmitHandler>,
57}
58
59impl Hooks {
60    /// Creates a new, empty `Hooks` container.
61    ///
62    /// Use this as the starting point for building a hooks collection using the builder pattern.
63    ///
64    /// # Example
65    ///
66    /// ```rust
67    /// use open_agent::Hooks;
68    ///
69    /// let hooks = Hooks::new()
70    ///     .add_pre_tool_use(|event| async move { None });
71    /// ```
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Registers a PreToolUse hook handler using the builder pattern.
77    ///
78    /// This method takes ownership of `self` and returns it back, allowing method chaining.
79    /// The handler is wrapped in `Arc` and added to the collection of PreToolUse hooks.
80    ///
81    /// # Parameters
82    ///
83    /// - `handler`: An async function or closure that takes `PreToolUseEvent` and returns
84    ///   `Option<HookDecision>`. Must be `Send + Sync + 'static` for thread safety.
85    ///
86    /// # Type Parameters
87    ///
88    /// - `F`: The function/closure type
89    /// - `Fut`: The future type returned by the function
90    ///
91    /// # Example
92    ///
93    /// ```rust
94    /// use open_agent::{Hooks, HookDecision};
95    ///
96    /// let hooks = Hooks::new()
97    ///     .add_pre_tool_use(|event| async move {
98    ///         println!("About to execute: {}", event.tool_name);
99    ///         None
100    ///     })
101    ///     .add_pre_tool_use(|event| async move {
102    ///         // This runs second (if first returns None)
103    ///         if event.tool_name == "blocked" {
104    ///             Some(HookDecision::block("Not allowed"))
105    ///         } else {
106    ///             None
107    ///         }
108    ///     });
109    /// ```
110    pub fn add_pre_tool_use<F, Fut>(mut self, handler: F) -> Self
111    where
112        F: Fn(PreToolUseEvent) -> Fut + Send + Sync + 'static,
113        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
114    {
115        // Wrap the user's function in Arc and Box::pin for type erasure and heap allocation
116        self.pre_tool_use
117            .push(Arc::new(move |event| Box::pin(handler(event))));
118        self
119    }
120
121    /// Registers a PostToolUse hook handler using the builder pattern.
122    ///
123    /// Identical to `add_pre_tool_use` but for PostToolUse events. See [`Self::add_pre_tool_use`]
124    /// for detailed documentation.
125    ///
126    /// # Example
127    ///
128    /// ```rust
129    /// use open_agent::Hooks;
130    ///
131    /// let hooks = Hooks::new()
132    ///     .add_post_tool_use(|event| async move {
133    ///         // Audit log all tool executions
134    ///         println!("Tool '{}' completed: {:?}",
135    ///                  event.tool_name, event.tool_result);
136    ///         None // Don't interfere with execution
137    ///     });
138    /// ```
139    pub fn add_post_tool_use<F, Fut>(mut self, handler: F) -> Self
140    where
141        F: Fn(PostToolUseEvent) -> Fut + Send + Sync + 'static,
142        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
143    {
144        // Wrap the user's function in Arc and Box::pin for type erasure and heap allocation
145        self.post_tool_use
146            .push(Arc::new(move |event| Box::pin(handler(event))));
147        self
148    }
149
150    /// Registers a UserPromptSubmit hook handler using the builder pattern.
151    ///
152    /// Identical to `add_pre_tool_use` but for UserPromptSubmit events. See [`Self::add_pre_tool_use`]
153    /// for detailed documentation.
154    ///
155    /// # Example
156    ///
157    /// ```rust
158    /// use open_agent::{Hooks, HookDecision};
159    ///
160    /// let hooks = Hooks::new()
161    ///     .add_user_prompt_submit(|event| async move {
162    ///         // Content moderation
163    ///         if event.prompt.contains("forbidden") {
164    ///             Some(HookDecision::block("Content violation"))
165    ///         } else {
166    ///             None
167    ///         }
168    ///     });
169    /// ```
170    pub fn add_user_prompt_submit<F, Fut>(mut self, handler: F) -> Self
171    where
172        F: Fn(UserPromptSubmitEvent) -> Fut + Send + Sync + 'static,
173        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
174    {
175        // Wrap the user's function in Arc and Box::pin for type erasure and heap allocation
176        self.user_prompt_submit
177            .push(Arc::new(move |event| Box::pin(handler(event))));
178        self
179    }
180
181    /// Executes all registered PreToolUse hooks in order and returns the first decision.
182    ///
183    /// This method implements the **"first non-None wins"** execution model:
184    ///
185    /// 1. Iterates through hooks in registration order (FIFO)
186    /// 2. Calls each hook with the same event snapshot
187    /// 3. If a hook returns `Some(decision)`, immediately returns that decision
188    /// 4. Remaining hooks are **not executed**
189    /// 5. If all hooks return `None`, returns `None`
190    ///
191    /// # Parameters
192    ///
193    /// - `event`: The PreToolUseEvent to pass to each hook
194    ///
195    /// # Returns
196    ///
197    /// - `Some(HookDecision)`: A hook made a decision (block, modify, or continue)
198    /// - `None`: All hooks returned `None` (continue normally)
199    ///
200    /// # Example
201    ///
202    /// ```rust
203    /// use open_agent::{Hooks, PreToolUseEvent, HookDecision};
204    /// use serde_json::json;
205    ///
206    /// # async fn example() {
207    /// let hooks = Hooks::new()
208    ///     .add_pre_tool_use(|e| async move { None }) // Runs first
209    ///     .add_pre_tool_use(|e| async move {
210    ///         Some(HookDecision::block("Blocked")) // Runs second, blocks
211    ///     })
212    ///     .add_pre_tool_use(|e| async move {
213    ///         None // NEVER runs because previous hook returned Some
214    ///     });
215    ///
216    /// let event = PreToolUseEvent::new(
217    ///     "test".to_string(),
218    ///     json!({}),
219    ///     "id".to_string(),
220    ///     vec![]
221    /// );
222    ///
223    /// let decision = hooks.execute_pre_tool_use(event).await;
224    /// assert!(decision.is_some());
225    /// assert!(!decision.unwrap().continue_execution());
226    /// # }
227    /// ```
228    pub async fn execute_pre_tool_use(&self, event: PreToolUseEvent) -> Option<HookDecision> {
229        let (last, preceding) = self.pre_tool_use.split_last()?;
230        for handler in preceding {
231            let decision = handler(event.clone()).await;
232            if decision.is_some() {
233                return decision;
234            }
235        }
236        last(event).await
237    }
238
239    /// Executes all registered PostToolUse hooks in order and returns the first decision.
240    ///
241    /// Identical in behavior to [`Self::execute_pre_tool_use`] but for PostToolUse events.
242    /// See that method for detailed documentation of the execution model.
243    ///
244    /// # Note
245    ///
246    /// PostToolUse hooks rarely return decisions in practice. They're primarily used for
247    /// observation (logging, metrics) and typically always return `None`.
248    pub async fn execute_post_tool_use(&self, event: PostToolUseEvent) -> Option<HookDecision> {
249        let (last, preceding) = self.post_tool_use.split_last()?;
250        for handler in preceding {
251            let decision = handler(event.clone()).await;
252            if decision.is_some() {
253                return decision;
254            }
255        }
256        last(event).await
257    }
258
259    /// Executes all registered UserPromptSubmit hooks in order and returns the first decision.
260    ///
261    /// Identical in behavior to [`Self::execute_pre_tool_use`] but for UserPromptSubmit events.
262    /// See that method for detailed documentation of the execution model.
263    pub async fn execute_user_prompt_submit(
264        &self,
265        event: UserPromptSubmitEvent,
266    ) -> Option<HookDecision> {
267        let (last, preceding) = self.user_prompt_submit.split_last()?;
268        for handler in preceding {
269            let decision = handler(event.clone()).await;
270            if decision.is_some() {
271                return decision;
272            }
273        }
274        last(event).await
275    }
276}
277
278/// Custom Debug implementation for Hooks.
279///
280/// Since hook handlers are closures (which don't implement Debug), we provide a custom
281/// implementation that shows the number of registered handlers instead of trying to
282/// debug-print the closures themselves.
283///
284/// # Example Output
285///
286/// ```text
287/// Hooks {
288///     pre_tool_use: 3 handlers,
289///     post_tool_use: 1 handlers,
290///     user_prompt_submit: 2 handlers
291/// }
292/// ```
293impl std::fmt::Debug for Hooks {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        f.debug_struct("Hooks")
296            .field(
297                "pre_tool_use",
298                &format!("{} handlers", self.pre_tool_use.len()),
299            )
300            .field(
301                "post_tool_use",
302                &format!("{} handlers", self.post_tool_use.len()),
303            )
304            .field(
305                "user_prompt_submit",
306                &format!("{} handlers", self.user_prompt_submit.len()),
307            )
308            .finish()
309    }
310}
311
312/// String constant for the PreToolUse hook event name.
313///
314/// This constant can be used for logging, metrics, or when you need a string
315/// representation of the hook type. It's primarily used internally but is exposed
316/// as part of the public API for consistency.
317pub const HOOK_PRE_TOOL_USE: &str = "pre_tool_use";
318
319/// String constant for the PostToolUse hook event name.
320///
321/// See [`HOOK_PRE_TOOL_USE`] for usage details.
322pub const HOOK_POST_TOOL_USE: &str = "post_tool_use";
323
324/// String constant for the UserPromptSubmit hook event name.
325///
326/// See [`HOOK_PRE_TOOL_USE`] for usage details.
327pub const HOOK_USER_PROMPT_SUBMIT: &str = "user_prompt_submit";