open_agent/hooks/decision.rs
1/// Decision returned by a hook handler to control agent execution flow.
2///
3/// When a hook returns `Some(HookDecision)`, it takes control of the execution flow.
4/// This struct determines whether execution should continue, whether inputs/prompts should
5/// be modified, and provides a reason for logging and debugging.
6///
7/// # "First Non-None Wins" Model
8///
9/// The hooks system uses a **sequential "first non-None wins"** execution model:
10///
11/// 1. Hooks are executed in the order they were registered
12/// 2. Each hook returns `Option<HookDecision>`:
13/// - `None` = "I don't care, let the next hook decide"
14/// - `Some(decision)` = "I'm taking control, stop checking other hooks"
15/// 3. The **first** hook that returns `Some(decision)` determines the outcome
16/// 4. Remaining hooks are **skipped** after a decision is made
17/// 5. If **all** hooks return `None`, execution continues normally
18///
19/// This model ensures:
20/// - Predictable behavior (order matters)
21/// - Performance (no unnecessary hook executions)
22/// - Priority (earlier hooks can't be overridden by later ones)
23///
24/// # Fields
25///
26/// - `continue_execution`: If `false`, abort the current operation (tool execution or prompt processing)
27/// - `modified_input`: For PreToolUse hooks - replaces the tool input with this value
28/// - `modified_prompt`: For UserPromptSubmit hooks - replaces the user prompt with this value
29/// - `reason`: Optional explanation for why this decision was made (useful for debugging/logging)
30///
31/// # Example: Hook Priority Order
32///
33/// ```rust
34/// use open_agent::{Hooks, PreToolUseEvent, HookDecision};
35///
36/// let hooks = Hooks::new()
37/// // First hook - security gate (highest priority)
38/// .add_pre_tool_use(|event| async move {
39/// if event.tool_name == "dangerous_tool" {
40/// // This blocks execution - later hooks won't run
41/// return Some(HookDecision::block("Blocked by security"));
42/// }
43/// None // Pass to next hook
44/// })
45/// // Second hook - rate limiting
46/// .add_pre_tool_use(|event| async move {
47/// // This only runs if first hook returned None
48/// if over_rate_limit(&event) {
49/// return Some(HookDecision::block("Rate limit exceeded"));
50/// }
51/// None
52/// })
53/// // Third hook - logging
54/// .add_pre_tool_use(|event| async move {
55/// // This only runs if previous hooks returned None
56/// println!("Tool {} called", event.tool_name);
57/// None // Always pass through
58/// });
59///
60/// fn over_rate_limit(_event: &PreToolUseEvent) -> bool { false }
61/// ```
62///
63/// # Builder Methods
64///
65/// The struct provides convenient builder methods for common scenarios:
66///
67/// - `HookDecision::continue_()` - Allow execution to proceed normally
68/// - `HookDecision::block(reason)` - Block execution with a reason
69/// - `HookDecision::modify_input(input, reason)` - Continue with modified tool input
70/// - `HookDecision::modify_prompt(prompt, reason)` - Continue with modified user prompt
71#[derive(Debug, Clone, Default)]
72pub struct HookDecision {
73 /// Whether to continue execution. If `false`, the operation is aborted.
74 /// Default: `false` (via Default trait), but builder methods set this appropriately.
75 continue_execution: bool,
76
77 /// For PreToolUse hooks: If set, replaces the original tool input with this value.
78 /// The tool will execute with this modified input instead of the original.
79 modified_input: Option<Value>,
80
81 /// For UserPromptSubmit hooks: If set, replaces the user's prompt with this value.
82 /// The agent will process this modified prompt instead of the original.
83 modified_prompt: Option<String>,
84
85 /// Optional human-readable explanation for why this decision was made.
86 /// Useful for logging, debugging, and audit trails.
87 reason: Option<String>,
88}
89
90impl HookDecision {
91 /// Creates a decision to continue execution normally without modifications.
92 ///
93 /// This is typically used when a hook wants to explicitly signal "continue" rather
94 /// than returning `None`. In most cases, returning `None` is simpler and preferred.
95 ///
96 /// # Example
97 ///
98 /// ```rust
99 /// use open_agent::{PreToolUseEvent, HookDecision};
100 ///
101 /// async fn my_hook(event: PreToolUseEvent) -> Option<HookDecision> {
102 /// // Log the tool use
103 /// println!("Tool called: {}", event.tool_name);
104 ///
105 /// // Explicitly continue (though returning None would be simpler)
106 /// Some(HookDecision::continue_())
107 /// }
108 /// ```
109 ///
110 /// Note: Named `continue_()` with trailing underscore because `continue` is a Rust keyword.
111 pub fn continue_() -> Self {
112 Self {
113 continue_execution: true,
114 modified_input: None,
115 modified_prompt: None,
116 reason: None,
117 }
118 }
119
120 /// Creates a decision to block execution with a reason.
121 ///
122 /// When a hook returns this decision, the current operation (tool execution or
123 /// prompt processing) is aborted, and the reason is logged.
124 ///
125 /// # Parameters
126 ///
127 /// - `reason`: Human-readable explanation for why execution was blocked
128 ///
129 /// # Example
130 ///
131 /// ```rust
132 /// use open_agent::{PreToolUseEvent, HookDecision};
133 ///
134 /// async fn security_gate(event: PreToolUseEvent) -> Option<HookDecision> {
135 /// if event.tool_name == "Bash" {
136 /// if let Some(cmd) = event.tool_input.get("command") {
137 /// if cmd.as_str()?.contains("rm -rf /") {
138 /// return Some(HookDecision::block(
139 /// "Dangerous recursive delete blocked"
140 /// ));
141 /// }
142 /// }
143 /// }
144 /// None
145 /// }
146 /// ```
147 pub fn block(reason: impl Into<String>) -> Self {
148 Self {
149 continue_execution: false,
150 modified_input: None,
151 modified_prompt: None,
152 reason: Some(reason.into()),
153 }
154 }
155
156 /// Creates a decision to modify tool input before execution.
157 ///
158 /// Use this in PreToolUse hooks to change the parameters that will be passed to the tool.
159 /// The tool will execute with the modified input instead of the original.
160 ///
161 /// # Parameters
162 ///
163 /// - `input`: The new tool input (as JSON Value) that replaces the original
164 /// - `reason`: Explanation for why the input was modified
165 ///
166 /// # Example
167 ///
168 /// ```rust
169 /// use open_agent::{PreToolUseEvent, HookDecision};
170 /// use serde_json::json;
171 ///
172 /// async fn inject_security_token(event: PreToolUseEvent) -> Option<HookDecision> {
173 /// if event.tool_name == "WebFetch" {
174 /// // Add authentication to all web requests
175 /// let mut modified = event.tool_input.clone();
176 /// modified["headers"] = json!({
177 /// "Authorization": "Bearer secret-token",
178 /// "X-User-ID": "user-123"
179 /// });
180 ///
181 /// return Some(HookDecision::modify_input(
182 /// modified,
183 /// "Injected authentication headers"
184 /// ));
185 /// }
186 /// None
187 /// }
188 /// ```
189 pub fn modify_input(input: Value, reason: impl Into<String>) -> Self {
190 Self {
191 continue_execution: true,
192 modified_input: Some(input),
193 modified_prompt: None,
194 reason: Some(reason.into()),
195 }
196 }
197
198 /// Creates a decision to modify the user's prompt before processing.
199 ///
200 /// Use this in UserPromptSubmit hooks to enhance, sanitize, or transform user input.
201 /// The agent will process the modified prompt instead of the original.
202 ///
203 /// # Parameters
204 ///
205 /// - `prompt`: The new prompt text that replaces the user's original input
206 /// - `reason`: Explanation for why the prompt was modified
207 ///
208 /// # Example
209 ///
210 /// ```rust
211 /// use open_agent::{UserPromptSubmitEvent, HookDecision};
212 ///
213 /// async fn add_context(event: UserPromptSubmitEvent) -> Option<HookDecision> {
214 /// // Add system context to every user prompt
215 /// let enhanced = format!(
216 /// "{}\n\n[System Context: You are in production mode. Be extra careful with destructive operations.]",
217 /// event.prompt
218 /// );
219 ///
220 /// Some(HookDecision::modify_prompt(
221 /// enhanced,
222 /// "Added production safety context"
223 /// ))
224 /// }
225 /// ```
226 ///
227 /// # Warning
228 ///
229 /// Modifying prompts can be confusing for users if done excessively or without clear
230 /// communication. Use this feature judiciously and consider logging modifications.
231 pub fn modify_prompt(prompt: impl Into<String>, reason: impl Into<String>) -> Self {
232 Self {
233 continue_execution: true,
234 modified_input: None,
235 modified_prompt: Some(prompt.into()),
236 reason: Some(reason.into()),
237 }
238 }
239
240 /// Returns whether execution should continue.
241 pub fn continue_execution(&self) -> bool {
242 self.continue_execution
243 }
244
245 /// Returns the modified input, if any.
246 pub fn modified_input(&self) -> Option<&Value> {
247 self.modified_input.as_ref()
248 }
249
250 /// Returns the modified prompt, if any.
251 pub fn modified_prompt(&self) -> Option<&str> {
252 self.modified_prompt.as_deref()
253 }
254
255 /// Returns the reason, if any.
256 pub fn reason(&self) -> Option<&str> {
257 self.reason.as_deref()
258 }
259}