Skip to main content

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}