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";