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