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}