open_agent/
hooks.rs

1//! Lifecycle Hooks System for Agent Execution Control
2//!
3//! This module provides a powerful hooks system for intercepting, monitoring, and controlling
4//! agent behavior at critical lifecycle points. Hooks enable you to implement security gates,
5//! audit logging, input validation, output filtering, and dynamic behavior modification without
6//! modifying the core agent logic.
7//!
8//! # Overview
9//!
10//! The hooks system operates on an event-driven model with three key interception points:
11//!
12//! 1. **PreToolUse**: Fired before any tool is executed, allowing you to:
13//!    - Block dangerous operations (security gates)
14//!    - Modify tool inputs (parameter injection, sanitization)
15//!    - Log tool usage for auditing
16//!    - Implement rate limiting or quotas
17//!
18//! 2. **PostToolUse**: Fired after tool execution completes, allowing you to:
19//!    - Audit tool results
20//!    - Filter or redact sensitive information in outputs
21//!    - Collect metrics and telemetry
22//!    - Validate tool behavior
23//!
24//! 3. **UserPromptSubmit**: Fired before processing user input, allowing you to:
25//!    - Filter inappropriate content
26//!    - Modify prompts (add context, instructions)
27//!    - Implement content moderation
28//!    - Track user interactions
29//!
30//! # Execution Model
31//!
32//! Hooks follow a **sequential "first non-None wins"** execution model:
33//! - Hooks are executed in the order they were registered
34//! - Each hook can return `None` (pass-through) or `Some(HookDecision)` (take control)
35//! - The **first hook** that returns `Some(HookDecision)` determines the outcome
36//! - Subsequent hooks are **not executed** after a decision is made
37//! - If all hooks return `None`, execution continues normally
38//!
39//! This model ensures predictable behavior and allows you to create hook chains where
40//! earlier hooks can implement critical security checks that later hooks cannot override.
41//!
42//! # Common Use Cases
43//!
44//! ## Security Gate (Block Dangerous Operations)
45//!
46//! ```rust,no_run
47//! use open_agent::{Hooks, PreToolUseEvent, HookDecision};
48//!
49//! let hooks = Hooks::new().add_pre_tool_use(|event| async move {
50//!     // Block file deletion in production
51//!     if event.tool_name == "delete_file" {
52//!         return Some(HookDecision::block("File deletion not allowed"));
53//!     }
54//!     None // Allow other operations
55//! });
56//! ```
57//!
58//! ## Audit Logging
59//!
60//! ```rust,no_run
61//! use open_agent::{Hooks, PostToolUseEvent, HookDecision};
62//!
63//! let hooks = Hooks::new().add_post_tool_use(|event| async move {
64//!     // Log all tool executions for compliance
65//!     println!("Tool '{}' executed with result: {:?}",
66//!              event.tool_name, event.tool_result);
67//!     None // Don't interfere with execution
68//! });
69//! ```
70//!
71//! ## Input Modification (Parameter Injection)
72//!
73//! ```rust,no_run
74//! use open_agent::{Hooks, PreToolUseEvent, HookDecision};
75//! use serde_json::json;
76//!
77//! let hooks = Hooks::new().add_pre_tool_use(|event| async move {
78//!     if event.tool_name == "query_database" {
79//!         // Inject security context into all database queries
80//!         let mut input = event.tool_input.clone();
81//!         input["user_id"] = json!("current_user_123");
82//!         return Some(HookDecision::modify_input(input, "Injected user context"));
83//!     }
84//!     None
85//! });
86//! ```
87//!
88//! ## Content Moderation
89//!
90//! ```rust,no_run
91//! use open_agent::{Hooks, UserPromptSubmitEvent, HookDecision};
92//!
93//! let hooks = Hooks::new().add_user_prompt_submit(|event| async move {
94//!     if event.prompt.contains("inappropriate_content") {
95//!         return Some(HookDecision::block("Content policy violation"));
96//!     }
97//!     None
98//! });
99//! ```
100//!
101//! ## Dynamic Prompt Enhancement
102//!
103//! ```ignore
104//! use open_agent::{Hooks, UserPromptSubmitEvent, HookDecision};
105//!
106//! let hooks = Hooks::new().add_user_prompt_submit(|event| async move {
107//!     // Add context to user prompts
108//!     let enhanced = format!(
109//!         "{}\n\nAdditional Context: Current time is {}",
110//!         event.prompt,
111//!         chrono::Utc::now()
112//!     );
113//!     Some(HookDecision::modify_prompt(enhanced, "Added timestamp context"))
114//! });
115//! ```
116//!
117//! # Thread Safety and Async
118//!
119//! All hooks are async functions wrapped in `Arc` to enable:
120//! - **Thread-safe sharing** across multiple agent instances
121//! - **Async operations** like database queries, API calls, or file I/O
122//! - **Zero-cost cloning** when passing hooks between threads
123//!
124//! Hooks can safely perform I/O operations, make network requests, or access shared state
125//! as long as that state is thread-safe (e.g., wrapped in `Arc<Mutex<T>>`).
126//!
127//! # Error Handling
128//!
129//! If a hook panics or returns an error, the entire agent operation will be aborted.
130//! Design your hooks to be robust and handle errors gracefully within the hook itself:
131//!
132//! ```rust,no_run
133//! use open_agent::{Hooks, PreToolUseEvent, HookDecision};
134//!
135//! let hooks = Hooks::new().add_pre_tool_use(|event| async move {
136//!     match risky_validation(&event).await {
137//!         Ok(is_valid) => {
138//!             if !is_valid {
139//!                 Some(HookDecision::block("Validation failed"))
140//!             } else {
141//!                 None
142//!             }
143//!         }
144//!         Err(e) => {
145//!             eprintln!("Hook validation error: {}", e);
146//!             // Fail safe: block on errors
147//!             Some(HookDecision::block(format!("Validation error: {}", e)))
148//!         }
149//!     }
150//! });
151//!
152//! async fn risky_validation(_event: &PreToolUseEvent) -> Result<bool, String> {
153//!     // Your validation logic here
154//!     Ok(true)
155//! }
156//! ```
157
158use serde_json::Value;
159use std::future::Future;
160use std::pin::Pin;
161use std::sync::Arc;
162
163/// Event fired **before** a tool is executed, enabling validation, modification, or blocking.
164///
165/// This event provides complete visibility into the tool that's about to be executed,
166/// allowing you to implement security policies, modify inputs, or collect telemetry
167/// before any potentially dangerous or expensive operations occur.
168///
169/// # Use Cases
170///
171/// - **Security gates**: Block dangerous operations (file deletion, network access)
172/// - **Input validation**: Ensure tool inputs meet schema or business rules
173/// - **Parameter injection**: Add authentication tokens, user context, or default values
174/// - **Rate limiting**: Track and limit tool usage per user/session
175/// - **Audit logging**: Record who is calling what tools with what parameters
176///
177/// # Fields
178///
179/// - `tool_name`: The name of the tool about to execute (e.g., "Bash", "Read", "WebFetch")
180/// - `tool_input`: The parameters that will be passed to the tool (as JSON)
181/// - `tool_use_id`: Unique identifier for this specific tool invocation
182/// - `history`: Read-only snapshot of the conversation history up to this point
183///
184/// # Example: Security Gate
185///
186/// ```rust
187/// use open_agent::{PreToolUseEvent, HookDecision};
188/// use serde_json::json;
189///
190/// async fn security_gate(event: PreToolUseEvent) -> Option<HookDecision> {
191///     // Block all Bash commands containing 'rm -rf'
192///     if event.tool_name == "Bash" {
193///         if let Some(command) = event.tool_input.get("command") {
194///             if command.as_str()?.contains("rm -rf") {
195///                 return Some(HookDecision::block(
196///                     "Dangerous command blocked for safety"
197///                 ));
198///             }
199///         }
200///     }
201///     None // Allow other tools
202/// }
203/// ```
204///
205/// # Example: Parameter Injection
206///
207/// ```rust
208/// use open_agent::{PreToolUseEvent, HookDecision};
209/// use serde_json::json;
210///
211/// async fn inject_auth(event: PreToolUseEvent) -> Option<HookDecision> {
212///     // Add authentication header to all API calls
213///     if event.tool_name == "WebFetch" {
214///         let mut modified = event.tool_input.clone();
215///         modified["headers"] = json!({
216///             "Authorization": "Bearer secret-token"
217///         });
218///         return Some(HookDecision::modify_input(
219///             modified,
220///             "Injected auth token"
221///         ));
222///     }
223///     None
224/// }
225/// ```
226#[derive(Debug, Clone)]
227pub struct PreToolUseEvent {
228    /// Name of the tool about to be executed (e.g., "Bash", "Read", "Edit")
229    pub tool_name: String,
230    /// Input parameters for the tool as a JSON value
231    pub tool_input: Value,
232    /// Unique identifier for this tool use (for correlation with PostToolUseEvent)
233    pub tool_use_id: String,
234    /// Snapshot of conversation history (read-only) - useful for context-aware decisions
235    pub history: Vec<Value>,
236}
237
238impl PreToolUseEvent {
239    /// Creates a new PreToolUseEvent.
240    ///
241    /// This constructor is typically called by the agent runtime, not by user code.
242    /// Users receive instances of this struct in their hook handlers.
243    pub fn new(
244        tool_name: String,
245        tool_input: Value,
246        tool_use_id: String,
247        history: Vec<Value>,
248    ) -> Self {
249        Self {
250            tool_name,
251            tool_input,
252            tool_use_id,
253            history,
254        }
255    }
256}
257
258/// Event fired **after** a tool completes execution, enabling audit, filtering, or validation.
259///
260/// This event provides complete visibility into what a tool did, including both the input
261/// parameters and the output result. Use this for auditing, metrics collection, output
262/// filtering, or post-execution validation.
263///
264/// # Use Cases
265///
266/// - **Audit logging**: Record all tool executions with inputs and outputs for compliance
267/// - **Output filtering**: Redact sensitive information from tool results
268/// - **Metrics collection**: Track tool performance, success rates, error patterns
269/// - **Result validation**: Ensure tool outputs meet quality or safety standards
270/// - **Error handling**: Implement custom error recovery or alerting
271///
272/// # Fields
273///
274/// - `tool_name`: The name of the tool that was executed
275/// - `tool_input`: The parameters that were actually used (may have been modified by PreToolUse hooks)
276/// - `tool_use_id`: Unique identifier for this invocation (matches PreToolUseEvent.tool_use_id)
277/// - `tool_result`: The result returned by the tool (contains either success data or error info)
278/// - `history`: Read-only snapshot of conversation history including this tool's execution
279///
280/// # Example: Audit Logging
281///
282/// ```rust
283/// use open_agent::{PostToolUseEvent, HookDecision};
284///
285/// async fn audit_logger(event: PostToolUseEvent) -> Option<HookDecision> {
286///     // Log all tool executions to your audit system
287///     let is_error = event.tool_result.get("error").is_some();
288///
289///     println!(
290///         "[AUDIT] Tool: {}, ID: {}, Status: {}",
291///         event.tool_name,
292///         event.tool_use_id,
293///         if is_error { "ERROR" } else { "SUCCESS" }
294///     );
295///
296///     // Send to external logging service
297///     // log_to_service(&event).await;
298///
299///     None // Don't interfere with execution
300/// }
301/// ```
302///
303/// # Example: Sensitive Data Redaction
304///
305/// ```rust
306/// use open_agent::{PostToolUseEvent, HookDecision};
307/// use serde_json::json;
308///
309/// async fn redact_secrets(event: PostToolUseEvent) -> Option<HookDecision> {
310///     // Redact API keys from Read tool output
311///     if event.tool_name == "Read" {
312///         if let Some(content) = event.tool_result.get("content") {
313///             if let Some(text) = content.as_str() {
314///                 if text.contains("API_KEY=") {
315///                     let redacted = text.replace(
316///                         |c: char| c.is_alphanumeric(),
317///                         "*"
318///                     );
319///                     // Note: PostToolUse hooks typically don't modify results,
320///                     // but you could log this for security review
321///                     println!("Warning: Potential API key detected in output");
322///                 }
323///             }
324///         }
325///     }
326///     None
327/// }
328/// ```
329///
330/// # Note on Modification
331///
332/// While `HookDecision` theoretically allows modification in PostToolUse hooks, this is
333/// rarely used in practice. The tool has already executed, and most agents don't support
334/// modifying historical results. PostToolUse hooks are primarily for observation and auditing.
335#[derive(Debug, Clone)]
336pub struct PostToolUseEvent {
337    /// Name of the tool that was executed
338    pub tool_name: String,
339    /// Input parameters that were actually used (may differ from original if modified by PreToolUse)
340    pub tool_input: Value,
341    /// Unique identifier for this tool use (correlates with PreToolUseEvent)
342    pub tool_use_id: String,
343    /// Result returned by the tool - may contain "content" on success or "error" on failure
344    pub tool_result: Value,
345    /// Snapshot of conversation history (read-only) including this tool execution
346    pub history: Vec<Value>,
347}
348
349impl PostToolUseEvent {
350    /// Creates a new PostToolUseEvent.
351    ///
352    /// This constructor is typically called by the agent runtime after tool execution,
353    /// not by user code. Users receive instances of this struct in their hook handlers.
354    pub fn new(
355        tool_name: String,
356        tool_input: Value,
357        tool_use_id: String,
358        tool_result: Value,
359        history: Vec<Value>,
360    ) -> Self {
361        Self {
362            tool_name,
363            tool_input,
364            tool_use_id,
365            tool_result,
366            history,
367        }
368    }
369}
370
371/// Event fired **before** processing user input, enabling content moderation and prompt enhancement.
372///
373/// This event is triggered whenever a user submits a prompt to the agent, before the agent
374/// begins processing it. Use this to implement content moderation, add context, inject
375/// instructions, or track user interactions.
376///
377/// # Use Cases
378///
379/// - **Content moderation**: Filter inappropriate or harmful user inputs
380/// - **Prompt enhancement**: Add system context, timestamps, or user information
381/// - **Input validation**: Ensure prompts meet format or length requirements
382/// - **Usage tracking**: Log user interactions for analytics or billing
383/// - **Context injection**: Add relevant background information to every prompt
384///
385/// # Fields
386///
387/// - `prompt`: The user's original input text
388/// - `history`: Read-only snapshot of the conversation history before this prompt
389///
390/// # Example: Content Moderation
391///
392/// ```rust
393/// use open_agent::{UserPromptSubmitEvent, HookDecision};
394///
395/// async fn content_moderator(event: UserPromptSubmitEvent) -> Option<HookDecision> {
396///     // Block prompts containing banned words
397///     let banned_words = ["spam", "malware", "hack"];
398///
399///     for word in banned_words {
400///         if event.prompt.to_lowercase().contains(word) {
401///             return Some(HookDecision::block(
402///                 format!("Content policy violation: contains '{}'", word)
403///             ));
404///         }
405///     }
406///     None // Allow clean prompts
407/// }
408/// ```
409///
410/// # Example: Automatic Context Enhancement
411///
412/// ```rust
413/// use open_agent::{UserPromptSubmitEvent, HookDecision};
414///
415/// async fn add_context(event: UserPromptSubmitEvent) -> Option<HookDecision> {
416///     // Add helpful context to every user prompt
417///     let enhanced = format!(
418///         "{}\n\n---\nContext: User timezone is UTC, current session started at 2025-11-07",
419///         event.prompt
420///     );
421///
422///     Some(HookDecision::modify_prompt(
423///         enhanced,
424///         "Added session context"
425///     ))
426/// }
427/// ```
428///
429/// # Example: Usage Tracking
430///
431/// ```rust
432/// use open_agent::{UserPromptSubmitEvent, HookDecision};
433///
434/// async fn track_usage(event: UserPromptSubmitEvent) -> Option<HookDecision> {
435///     // Log every user interaction for analytics
436///     println!(
437///         "[ANALYTICS] User submitted prompt of {} characters at history depth {}",
438///         event.prompt.len(),
439///         event.history.len()
440///     );
441///
442///     // Could also:
443///     // - Update usage quotas
444///     // - Send to analytics service
445///     // - Check rate limits
446///
447///     None // Don't modify the prompt
448/// }
449/// ```
450///
451/// # Modification Behavior
452///
453/// If you return `HookDecision::modify_prompt()`, the modified prompt completely replaces
454/// the original user input before the agent processes it. This is powerful but should be
455/// used carefully to avoid confusing the user or the agent.
456#[derive(Debug, Clone)]
457pub struct UserPromptSubmitEvent {
458    /// The user's original input prompt text
459    pub prompt: String,
460    /// Snapshot of conversation history (read-only) - does not include this prompt yet
461    pub history: Vec<Value>,
462}
463
464impl UserPromptSubmitEvent {
465    /// Creates a new UserPromptSubmitEvent.
466    ///
467    /// This constructor is typically called by the agent runtime when processing user input,
468    /// not by user code. Users receive instances of this struct in their hook handlers.
469    pub fn new(prompt: String, history: Vec<Value>) -> Self {
470        Self { prompt, history }
471    }
472}
473
474/// Decision returned by a hook handler to control agent execution flow.
475///
476/// When a hook returns `Some(HookDecision)`, it takes control of the execution flow.
477/// This struct determines whether execution should continue, whether inputs/prompts should
478/// be modified, and provides a reason for logging and debugging.
479///
480/// # "First Non-None Wins" Model
481///
482/// The hooks system uses a **sequential "first non-None wins"** execution model:
483///
484/// 1. Hooks are executed in the order they were registered
485/// 2. Each hook returns `Option<HookDecision>`:
486///    - `None` = "I don't care, let the next hook decide"
487///    - `Some(decision)` = "I'm taking control, stop checking other hooks"
488/// 3. The **first** hook that returns `Some(decision)` determines the outcome
489/// 4. Remaining hooks are **skipped** after a decision is made
490/// 5. If **all** hooks return `None`, execution continues normally
491///
492/// This model ensures:
493/// - Predictable behavior (order matters)
494/// - Performance (no unnecessary hook executions)
495/// - Priority (earlier hooks can't be overridden by later ones)
496///
497/// # Fields
498///
499/// - `continue_execution`: If `false`, abort the current operation (tool execution or prompt processing)
500/// - `modified_input`: For PreToolUse hooks - replaces the tool input with this value
501/// - `modified_prompt`: For UserPromptSubmit hooks - replaces the user prompt with this value
502/// - `reason`: Optional explanation for why this decision was made (useful for debugging/logging)
503///
504/// # Example: Hook Priority Order
505///
506/// ```rust
507/// use open_agent::{Hooks, PreToolUseEvent, HookDecision};
508///
509/// let hooks = Hooks::new()
510///     // First hook - security gate (highest priority)
511///     .add_pre_tool_use(|event| async move {
512///         if event.tool_name == "dangerous_tool" {
513///             // This blocks execution - later hooks won't run
514///             return Some(HookDecision::block("Blocked by security"));
515///         }
516///         None // Pass to next hook
517///     })
518///     // Second hook - rate limiting
519///     .add_pre_tool_use(|event| async move {
520///         // This only runs if first hook returned None
521///         if over_rate_limit(&event) {
522///             return Some(HookDecision::block("Rate limit exceeded"));
523///         }
524///         None
525///     })
526///     // Third hook - logging
527///     .add_pre_tool_use(|event| async move {
528///         // This only runs if previous hooks returned None
529///         println!("Tool {} called", event.tool_name);
530///         None // Always pass through
531///     });
532///
533/// fn over_rate_limit(_event: &PreToolUseEvent) -> bool { false }
534/// ```
535///
536/// # Builder Methods
537///
538/// The struct provides convenient builder methods for common scenarios:
539///
540/// - `HookDecision::continue_()` - Allow execution to proceed normally
541/// - `HookDecision::block(reason)` - Block execution with a reason
542/// - `HookDecision::modify_input(input, reason)` - Continue with modified tool input
543/// - `HookDecision::modify_prompt(prompt, reason)` - Continue with modified user prompt
544#[derive(Debug, Clone, Default)]
545pub struct HookDecision {
546    /// Whether to continue execution. If `false`, the operation is aborted.
547    /// Default: `false` (via Default trait), but builder methods set this appropriately.
548    continue_execution: bool,
549
550    /// For PreToolUse hooks: If set, replaces the original tool input with this value.
551    /// The tool will execute with this modified input instead of the original.
552    modified_input: Option<Value>,
553
554    /// For UserPromptSubmit hooks: If set, replaces the user's prompt with this value.
555    /// The agent will process this modified prompt instead of the original.
556    modified_prompt: Option<String>,
557
558    /// Optional human-readable explanation for why this decision was made.
559    /// Useful for logging, debugging, and audit trails.
560    reason: Option<String>,
561}
562
563impl HookDecision {
564    /// Creates a decision to continue execution normally without modifications.
565    ///
566    /// This is typically used when a hook wants to explicitly signal "continue" rather
567    /// than returning `None`. In most cases, returning `None` is simpler and preferred.
568    ///
569    /// # Example
570    ///
571    /// ```rust
572    /// use open_agent::{PreToolUseEvent, HookDecision};
573    ///
574    /// async fn my_hook(event: PreToolUseEvent) -> Option<HookDecision> {
575    ///     // Log the tool use
576    ///     println!("Tool called: {}", event.tool_name);
577    ///
578    ///     // Explicitly continue (though returning None would be simpler)
579    ///     Some(HookDecision::continue_())
580    /// }
581    /// ```
582    ///
583    /// Note: Named `continue_()` with trailing underscore because `continue` is a Rust keyword.
584    pub fn continue_() -> Self {
585        Self {
586            continue_execution: true,
587            modified_input: None,
588            modified_prompt: None,
589            reason: None,
590        }
591    }
592
593    /// Creates a decision to block execution with a reason.
594    ///
595    /// When a hook returns this decision, the current operation (tool execution or
596    /// prompt processing) is aborted, and the reason is logged.
597    ///
598    /// # Parameters
599    ///
600    /// - `reason`: Human-readable explanation for why execution was blocked
601    ///
602    /// # Example
603    ///
604    /// ```rust
605    /// use open_agent::{PreToolUseEvent, HookDecision};
606    ///
607    /// async fn security_gate(event: PreToolUseEvent) -> Option<HookDecision> {
608    ///     if event.tool_name == "Bash" {
609    ///         if let Some(cmd) = event.tool_input.get("command") {
610    ///             if cmd.as_str()?.contains("rm -rf /") {
611    ///                 return Some(HookDecision::block(
612    ///                     "Dangerous recursive delete blocked"
613    ///                 ));
614    ///             }
615    ///         }
616    ///     }
617    ///     None
618    /// }
619    /// ```
620    pub fn block(reason: impl Into<String>) -> Self {
621        Self {
622            continue_execution: false,
623            modified_input: None,
624            modified_prompt: None,
625            reason: Some(reason.into()),
626        }
627    }
628
629    /// Creates a decision to modify tool input before execution.
630    ///
631    /// Use this in PreToolUse hooks to change the parameters that will be passed to the tool.
632    /// The tool will execute with the modified input instead of the original.
633    ///
634    /// # Parameters
635    ///
636    /// - `input`: The new tool input (as JSON Value) that replaces the original
637    /// - `reason`: Explanation for why the input was modified
638    ///
639    /// # Example
640    ///
641    /// ```rust
642    /// use open_agent::{PreToolUseEvent, HookDecision};
643    /// use serde_json::json;
644    ///
645    /// async fn inject_security_token(event: PreToolUseEvent) -> Option<HookDecision> {
646    ///     if event.tool_name == "WebFetch" {
647    ///         // Add authentication to all web requests
648    ///         let mut modified = event.tool_input.clone();
649    ///         modified["headers"] = json!({
650    ///             "Authorization": "Bearer secret-token",
651    ///             "X-User-ID": "user-123"
652    ///         });
653    ///
654    ///         return Some(HookDecision::modify_input(
655    ///             modified,
656    ///             "Injected authentication headers"
657    ///         ));
658    ///     }
659    ///     None
660    /// }
661    /// ```
662    pub fn modify_input(input: Value, reason: impl Into<String>) -> Self {
663        Self {
664            continue_execution: true,
665            modified_input: Some(input),
666            modified_prompt: None,
667            reason: Some(reason.into()),
668        }
669    }
670
671    /// Creates a decision to modify the user's prompt before processing.
672    ///
673    /// Use this in UserPromptSubmit hooks to enhance, sanitize, or transform user input.
674    /// The agent will process the modified prompt instead of the original.
675    ///
676    /// # Parameters
677    ///
678    /// - `prompt`: The new prompt text that replaces the user's original input
679    /// - `reason`: Explanation for why the prompt was modified
680    ///
681    /// # Example
682    ///
683    /// ```rust
684    /// use open_agent::{UserPromptSubmitEvent, HookDecision};
685    ///
686    /// async fn add_context(event: UserPromptSubmitEvent) -> Option<HookDecision> {
687    ///     // Add system context to every user prompt
688    ///     let enhanced = format!(
689    ///         "{}\n\n[System Context: You are in production mode. Be extra careful with destructive operations.]",
690    ///         event.prompt
691    ///     );
692    ///
693    ///     Some(HookDecision::modify_prompt(
694    ///         enhanced,
695    ///         "Added production safety context"
696    ///     ))
697    /// }
698    /// ```
699    ///
700    /// # Warning
701    ///
702    /// Modifying prompts can be confusing for users if done excessively or without clear
703    /// communication. Use this feature judiciously and consider logging modifications.
704    pub fn modify_prompt(prompt: impl Into<String>, reason: impl Into<String>) -> Self {
705        Self {
706            continue_execution: true,
707            modified_input: None,
708            modified_prompt: Some(prompt.into()),
709            reason: Some(reason.into()),
710        }
711    }
712
713    /// Returns whether execution should continue.
714    pub fn continue_execution(&self) -> bool {
715        self.continue_execution
716    }
717
718    /// Returns the modified input, if any.
719    pub fn modified_input(&self) -> Option<&Value> {
720        self.modified_input.as_ref()
721    }
722
723    /// Returns the modified prompt, if any.
724    pub fn modified_prompt(&self) -> Option<&str> {
725        self.modified_prompt.as_deref()
726    }
727
728    /// Returns the reason, if any.
729    pub fn reason(&self) -> Option<&str> {
730        self.reason.as_deref()
731    }
732}
733
734/// Type alias for PreToolUse hook handler functions.
735///
736/// This complex type signature enables powerful async hook functionality while maintaining
737/// thread safety and zero-cost abstraction. Let's break it down:
738///
739/// # Type Breakdown
740///
741/// ```text
742/// Arc<                                  // Reference counting for thread-safe sharing
743///     dyn Fn(PreToolUseEvent)           // Function taking the event
744///         -> Pin<Box<                    // Heap-allocated, pinned future
745///             dyn Future<Output = Option<HookDecision>>  // Async result
746///                 + Send                 // Can be sent across threads
747///         >>
748///         + Send + Sync                  // The function itself is thread-safe
749/// >
750/// ```
751///
752/// # Why This Design?
753///
754/// - **`Arc`**: Enables zero-cost cloning when passing hooks between threads or agent instances.
755///   Multiple agents can share the same hook without duplicating memory.
756///
757/// - **`dyn Fn`**: Allows any function or closure to be used as a hook, as long as it matches
758///   the signature. This is trait object type erasure.
759///
760/// - **`Pin<Box<dyn Future>>`**: Async functions in Rust return opaque Future types. We need
761///   to box them for dynamic dispatch and pin them because futures may contain self-references.
762///
763/// - **`Send + Sync`**: Ensures the hook can be safely called from multiple threads. Essential
764///   for async runtimes like Tokio that may schedule tasks on different threads.
765///
766/// # Return Value
767///
768/// Hook handlers return `Option<HookDecision>`:
769/// - `None`: "I don't care, continue normally or let next hook decide"
770/// - `Some(HookDecision)`: "I'm taking control" - blocks remaining hooks from running
771///
772/// # Example Usage
773///
774/// You don't typically construct these types directly. Instead, use the builder methods:
775///
776/// ```rust
777/// use open_agent::{Hooks, PreToolUseEvent, HookDecision};
778///
779/// let hooks = Hooks::new().add_pre_tool_use(|event| async move {
780///     // Your async logic here
781///     if event.tool_name == "dangerous" {
782///         Some(HookDecision::block("Not allowed"))
783///     } else {
784///         None
785///     }
786/// });
787/// ```
788///
789/// The builder automatically wraps your closure in `Arc<...>` and handles the `Pin<Box<...>>`.
790pub type PreToolUseHandler = Arc<
791    dyn Fn(PreToolUseEvent) -> Pin<Box<dyn Future<Output = Option<HookDecision>> + Send>>
792        + Send
793        + Sync,
794>;
795
796/// Type alias for PostToolUse hook handler functions.
797///
798/// Identical in structure to `PreToolUseHandler` but receives `PostToolUseEvent` instead.
799/// See [`PreToolUseHandler`] for detailed explanation of the type signature.
800///
801/// # Common Usage Pattern
802///
803/// PostToolUse hooks typically don't modify execution (they return `None`) but are used
804/// for observation, logging, and metrics:
805///
806/// ```rust
807/// use open_agent::{Hooks, PostToolUseEvent, HookDecision};
808///
809/// let hooks = Hooks::new().add_post_tool_use(|event| async move {
810///     // Log tool execution for audit trail
811///     println!("Tool {} completed with result: {:?}",
812///              event.tool_name, event.tool_result);
813///
814///     // Send metrics to monitoring system
815///     // metrics::counter!("tool_executions", 1, "tool" => event.tool_name);
816///
817///     None // Don't interfere with execution
818/// });
819/// ```
820pub type PostToolUseHandler = Arc<
821    dyn Fn(PostToolUseEvent) -> Pin<Box<dyn Future<Output = Option<HookDecision>> + Send>>
822        + Send
823        + Sync,
824>;
825
826/// Type alias for UserPromptSubmit hook handler functions.
827///
828/// Identical in structure to `PreToolUseHandler` but receives `UserPromptSubmitEvent` instead.
829/// See [`PreToolUseHandler`] for detailed explanation of the type signature.
830///
831/// # Common Usage Pattern
832///
833/// UserPromptSubmit hooks are often used for content moderation and prompt enhancement:
834///
835/// ```rust
836/// use open_agent::{Hooks, UserPromptSubmitEvent, HookDecision};
837///
838/// let hooks = Hooks::new().add_user_prompt_submit(|event| async move {
839///     // Block inappropriate content
840///     if event.prompt.to_lowercase().contains("banned_word") {
841///         return Some(HookDecision::block("Content policy violation"));
842///     }
843///
844///     // Or enhance prompts with context
845///     let enhanced = format!("{}\n\nContext: Session ID 12345", event.prompt);
846///     Some(HookDecision::modify_prompt(enhanced, "Added session context"))
847/// });
848/// ```
849pub type UserPromptSubmitHandler = Arc<
850    dyn Fn(UserPromptSubmitEvent) -> Pin<Box<dyn Future<Output = Option<HookDecision>> + Send>>
851        + Send
852        + Sync,
853>;
854
855/// Container for registering and managing lifecycle hooks.
856///
857/// The `Hooks` struct stores collections of hook handlers for different lifecycle events.
858/// It provides a builder pattern for registering hooks and executor methods for running them.
859///
860/// # Design Principles
861///
862/// - **Builder Pattern**: Hooks can be chained during construction using `.add_*()` methods
863/// - **Multiple Hooks**: You can register multiple hooks for the same event type
864/// - **Execution Order**: Hooks execute in the order they were registered (FIFO)
865/// - **First Wins**: The first hook returning `Some(HookDecision)` determines the outcome
866/// - **Thread Safe**: The struct is `Clone` and all handlers are `Arc`-wrapped for sharing
867///
868/// # Example: Building a Hooks Collection
869///
870/// ```rust
871/// use open_agent::{Hooks, PreToolUseEvent, PostToolUseEvent, HookDecision};
872///
873/// let hooks = Hooks::new()
874///     // First: Security gate (highest priority)
875///     .add_pre_tool_use(|event| async move {
876///         if event.tool_name == "dangerous" {
877///             return Some(HookDecision::block("Security violation"));
878///         }
879///         None
880///     })
881///     // Second: Rate limiting
882///     .add_pre_tool_use(|event| async move {
883///         // Check rate limits...
884///         None
885///     })
886///     // Audit logging (happens after execution)
887///     .add_post_tool_use(|event| async move {
888///         println!("Tool '{}' executed", event.tool_name);
889///         None
890///     });
891/// ```
892///
893/// # Fields
894///
895/// - `pre_tool_use`: Handlers invoked before tool execution
896/// - `post_tool_use`: Handlers invoked after tool execution
897/// - `user_prompt_submit`: Handlers invoked before processing user prompts
898///
899/// All fields are public, allowing direct manipulation if needed, though the builder
900/// methods are the recommended approach.
901#[derive(Clone, Default)]
902pub struct Hooks {
903    /// Collection of PreToolUse hook handlers, executed in registration order
904    pub pre_tool_use: Vec<PreToolUseHandler>,
905
906    /// Collection of PostToolUse hook handlers, executed in registration order
907    pub post_tool_use: Vec<PostToolUseHandler>,
908
909    /// Collection of UserPromptSubmit hook handlers, executed in registration order
910    pub user_prompt_submit: Vec<UserPromptSubmitHandler>,
911}
912
913impl Hooks {
914    /// Creates a new, empty `Hooks` container.
915    ///
916    /// Use this as the starting point for building a hooks collection using the builder pattern.
917    ///
918    /// # Example
919    ///
920    /// ```rust
921    /// use open_agent::Hooks;
922    ///
923    /// let hooks = Hooks::new()
924    ///     .add_pre_tool_use(|event| async move { None });
925    /// ```
926    pub fn new() -> Self {
927        Self::default()
928    }
929
930    /// Registers a PreToolUse hook handler using the builder pattern.
931    ///
932    /// This method takes ownership of `self` and returns it back, allowing method chaining.
933    /// The handler is wrapped in `Arc` and added to the collection of PreToolUse hooks.
934    ///
935    /// # Parameters
936    ///
937    /// - `handler`: An async function or closure that takes `PreToolUseEvent` and returns
938    ///   `Option<HookDecision>`. Must be `Send + Sync + 'static` for thread safety.
939    ///
940    /// # Type Parameters
941    ///
942    /// - `F`: The function/closure type
943    /// - `Fut`: The future type returned by the function
944    ///
945    /// # Example
946    ///
947    /// ```rust
948    /// use open_agent::{Hooks, HookDecision};
949    ///
950    /// let hooks = Hooks::new()
951    ///     .add_pre_tool_use(|event| async move {
952    ///         println!("About to execute: {}", event.tool_name);
953    ///         None
954    ///     })
955    ///     .add_pre_tool_use(|event| async move {
956    ///         // This runs second (if first returns None)
957    ///         if event.tool_name == "blocked" {
958    ///             Some(HookDecision::block("Not allowed"))
959    ///         } else {
960    ///             None
961    ///         }
962    ///     });
963    /// ```
964    pub fn add_pre_tool_use<F, Fut>(mut self, handler: F) -> Self
965    where
966        F: Fn(PreToolUseEvent) -> Fut + Send + Sync + 'static,
967        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
968    {
969        // Wrap the user's function in Arc and Box::pin for type erasure and heap allocation
970        self.pre_tool_use
971            .push(Arc::new(move |event| Box::pin(handler(event))));
972        self
973    }
974
975    /// Registers a PostToolUse hook handler using the builder pattern.
976    ///
977    /// Identical to `add_pre_tool_use` but for PostToolUse events. See [`Self::add_pre_tool_use`]
978    /// for detailed documentation.
979    ///
980    /// # Example
981    ///
982    /// ```rust
983    /// use open_agent::Hooks;
984    ///
985    /// let hooks = Hooks::new()
986    ///     .add_post_tool_use(|event| async move {
987    ///         // Audit log all tool executions
988    ///         println!("Tool '{}' completed: {:?}",
989    ///                  event.tool_name, event.tool_result);
990    ///         None // Don't interfere with execution
991    ///     });
992    /// ```
993    pub fn add_post_tool_use<F, Fut>(mut self, handler: F) -> Self
994    where
995        F: Fn(PostToolUseEvent) -> Fut + Send + Sync + 'static,
996        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
997    {
998        // Wrap the user's function in Arc and Box::pin for type erasure and heap allocation
999        self.post_tool_use
1000            .push(Arc::new(move |event| Box::pin(handler(event))));
1001        self
1002    }
1003
1004    /// Registers a UserPromptSubmit hook handler using the builder pattern.
1005    ///
1006    /// Identical to `add_pre_tool_use` but for UserPromptSubmit events. See [`Self::add_pre_tool_use`]
1007    /// for detailed documentation.
1008    ///
1009    /// # Example
1010    ///
1011    /// ```rust
1012    /// use open_agent::{Hooks, HookDecision};
1013    ///
1014    /// let hooks = Hooks::new()
1015    ///     .add_user_prompt_submit(|event| async move {
1016    ///         // Content moderation
1017    ///         if event.prompt.contains("forbidden") {
1018    ///             Some(HookDecision::block("Content violation"))
1019    ///         } else {
1020    ///             None
1021    ///         }
1022    ///     });
1023    /// ```
1024    pub fn add_user_prompt_submit<F, Fut>(mut self, handler: F) -> Self
1025    where
1026        F: Fn(UserPromptSubmitEvent) -> Fut + Send + Sync + 'static,
1027        Fut: Future<Output = Option<HookDecision>> + Send + 'static,
1028    {
1029        // Wrap the user's function in Arc and Box::pin for type erasure and heap allocation
1030        self.user_prompt_submit
1031            .push(Arc::new(move |event| Box::pin(handler(event))));
1032        self
1033    }
1034
1035    /// Executes all registered PreToolUse hooks in order and returns the first decision.
1036    ///
1037    /// This method implements the **"first non-None wins"** execution model:
1038    ///
1039    /// 1. Iterates through hooks in registration order (FIFO)
1040    /// 2. Calls each hook with a clone of the event
1041    /// 3. If a hook returns `Some(decision)`, immediately returns that decision
1042    /// 4. Remaining hooks are **not executed**
1043    /// 5. If all hooks return `None`, returns `None`
1044    ///
1045    /// # Parameters
1046    ///
1047    /// - `event`: The PreToolUseEvent to pass to each hook
1048    ///
1049    /// # Returns
1050    ///
1051    /// - `Some(HookDecision)`: A hook made a decision (block, modify, or continue)
1052    /// - `None`: All hooks returned `None` (continue normally)
1053    ///
1054    /// # Example
1055    ///
1056    /// ```rust
1057    /// use open_agent::{Hooks, PreToolUseEvent, HookDecision};
1058    /// use serde_json::json;
1059    ///
1060    /// # async fn example() {
1061    /// let hooks = Hooks::new()
1062    ///     .add_pre_tool_use(|e| async move { None }) // Runs first
1063    ///     .add_pre_tool_use(|e| async move {
1064    ///         Some(HookDecision::block("Blocked")) // Runs second, blocks
1065    ///     })
1066    ///     .add_pre_tool_use(|e| async move {
1067    ///         None // NEVER runs because previous hook returned Some
1068    ///     });
1069    ///
1070    /// let event = PreToolUseEvent::new(
1071    ///     "test".to_string(),
1072    ///     json!({}),
1073    ///     "id".to_string(),
1074    ///     vec![]
1075    /// );
1076    ///
1077    /// let decision = hooks.execute_pre_tool_use(event).await;
1078    /// assert!(decision.is_some());
1079    /// assert!(!decision.unwrap().continue_execution());
1080    /// # }
1081    /// ```
1082    pub async fn execute_pre_tool_use(&self, event: PreToolUseEvent) -> Option<HookDecision> {
1083        // Sequential execution: iterate through handlers in order
1084        for handler in &self.pre_tool_use {
1085            // Clone the event for this handler (events are cheaply cloneable)
1086            let decision = handler(event.clone()).await;
1087
1088            // First non-None wins: return immediately if this hook made a decision
1089            if decision.is_some() {
1090                return decision;
1091            }
1092            // Otherwise, continue to next hook
1093        }
1094
1095        // All hooks returned None: no decision made, continue normally
1096        None
1097    }
1098
1099    /// Executes all registered PostToolUse hooks in order and returns the first decision.
1100    ///
1101    /// Identical in behavior to [`Self::execute_pre_tool_use`] but for PostToolUse events.
1102    /// See that method for detailed documentation of the execution model.
1103    ///
1104    /// # Note
1105    ///
1106    /// PostToolUse hooks rarely return decisions in practice. They're primarily used for
1107    /// observation (logging, metrics) and typically always return `None`.
1108    pub async fn execute_post_tool_use(&self, event: PostToolUseEvent) -> Option<HookDecision> {
1109        // Sequential execution with "first non-None wins" model
1110        for handler in &self.post_tool_use {
1111            let decision = handler(event.clone()).await;
1112            if decision.is_some() {
1113                return decision;
1114            }
1115        }
1116        None
1117    }
1118
1119    /// Executes all registered UserPromptSubmit hooks in order and returns the first decision.
1120    ///
1121    /// Identical in behavior to [`Self::execute_pre_tool_use`] but for UserPromptSubmit events.
1122    /// See that method for detailed documentation of the execution model.
1123    pub async fn execute_user_prompt_submit(
1124        &self,
1125        event: UserPromptSubmitEvent,
1126    ) -> Option<HookDecision> {
1127        // Sequential execution with "first non-None wins" model
1128        for handler in &self.user_prompt_submit {
1129            let decision = handler(event.clone()).await;
1130            if decision.is_some() {
1131                return decision;
1132            }
1133        }
1134        None
1135    }
1136}
1137
1138/// Custom Debug implementation for Hooks.
1139///
1140/// Since hook handlers are closures (which don't implement Debug), we provide a custom
1141/// implementation that shows the number of registered handlers instead of trying to
1142/// debug-print the closures themselves.
1143///
1144/// # Example Output
1145///
1146/// ```text
1147/// Hooks {
1148///     pre_tool_use: 3 handlers,
1149///     post_tool_use: 1 handlers,
1150///     user_prompt_submit: 2 handlers
1151/// }
1152/// ```
1153impl std::fmt::Debug for Hooks {
1154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1155        f.debug_struct("Hooks")
1156            .field(
1157                "pre_tool_use",
1158                &format!("{} handlers", self.pre_tool_use.len()),
1159            )
1160            .field(
1161                "post_tool_use",
1162                &format!("{} handlers", self.post_tool_use.len()),
1163            )
1164            .field(
1165                "user_prompt_submit",
1166                &format!("{} handlers", self.user_prompt_submit.len()),
1167            )
1168            .finish()
1169    }
1170}
1171
1172/// String constant for the PreToolUse hook event name.
1173///
1174/// This constant can be used for logging, metrics, or when you need a string
1175/// representation of the hook type. It's primarily used internally but is exposed
1176/// as part of the public API for consistency.
1177pub const HOOK_PRE_TOOL_USE: &str = "pre_tool_use";
1178
1179/// String constant for the PostToolUse hook event name.
1180///
1181/// See [`HOOK_PRE_TOOL_USE`] for usage details.
1182pub const HOOK_POST_TOOL_USE: &str = "post_tool_use";
1183
1184/// String constant for the UserPromptSubmit hook event name.
1185///
1186/// See [`HOOK_PRE_TOOL_USE`] for usage details.
1187pub const HOOK_USER_PROMPT_SUBMIT: &str = "user_prompt_submit";
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192    use serde_json::json;
1193
1194    #[tokio::test]
1195    async fn test_hook_decision_builders() {
1196        let continue_dec = HookDecision::continue_();
1197        assert!(continue_dec.continue_execution);
1198        assert!(continue_dec.reason.is_none());
1199
1200        let block_dec = HookDecision::block("test");
1201        assert!(!block_dec.continue_execution);
1202        assert_eq!(block_dec.reason, Some("test".to_string()));
1203
1204        let modify_dec = HookDecision::modify_input(json!({"test": 1}), "modified");
1205        assert!(modify_dec.continue_execution);
1206        assert!(modify_dec.modified_input.is_some());
1207    }
1208
1209    #[tokio::test]
1210    async fn test_pre_tool_use_hook() {
1211        let hooks = Hooks::new().add_pre_tool_use(|event| async move {
1212            if event.tool_name == "dangerous" {
1213                return Some(HookDecision::block("blocked"));
1214            }
1215            None
1216        });
1217
1218        let event = PreToolUseEvent::new(
1219            "dangerous".to_string(),
1220            json!({}),
1221            "id1".to_string(),
1222            vec![],
1223        );
1224
1225        let decision = hooks.execute_pre_tool_use(event).await;
1226        assert!(decision.is_some());
1227        assert!(!decision.unwrap().continue_execution);
1228    }
1229
1230    #[tokio::test]
1231    async fn test_post_tool_use_hook() {
1232        let hooks = Hooks::new().add_post_tool_use(|_event| async move { None });
1233
1234        let event = PostToolUseEvent::new(
1235            "test".to_string(),
1236            json!({}),
1237            "id1".to_string(),
1238            json!({"result": "ok"}),
1239            vec![],
1240        );
1241
1242        // Should not panic
1243        hooks.execute_post_tool_use(event).await;
1244    }
1245
1246    #[tokio::test]
1247    async fn test_user_prompt_submit_hook() {
1248        let hooks = Hooks::new().add_user_prompt_submit(|event| async move {
1249            if event.prompt.contains("DELETE") {
1250                return Some(HookDecision::block("dangerous prompt"));
1251            }
1252            None
1253        });
1254
1255        let event = UserPromptSubmitEvent::new("DELETE all files".to_string(), vec![]);
1256
1257        let decision = hooks.execute_user_prompt_submit(event).await;
1258        assert!(decision.is_some());
1259        assert!(!decision.unwrap().continue_execution);
1260    }
1261}