Skip to main content

a3s_code_core/hooks/
mod.rs

1//! Hooks System for A3S Code Agent
2//!
3//! Provides a mechanism to intercept and customize agent behavior at various
4//! lifecycle points. Hooks can validate, transform, or block operations.
5//!
6//! ## Hook Events
7//!
8//! - `PreToolUse`: Before tool execution (can block/modify)
9//! - `PostToolUse`: After tool execution (fire-and-forget)
10//! - `GenerateStart`: Before LLM generation
11//! - `GenerateEnd`: After LLM generation
12//! - `SessionStart`: When session is created
13//! - `SessionEnd`: When session is destroyed
14//! - `PrePlanning`: Before task planning/decomposition (can block)
15//! - `PostPlanning`: After a plan is generated or planning fails
16//!
17//! ## Example
18//!
19//! ```ignore
20//! let engine = HookEngine::new();
21//!
22//! // Register a hook
23//! engine.register(Hook {
24//!     id: "security-check".to_string(),
25//!     event_type: HookEventType::PreToolUse,
26//!     matcher: Some(HookMatcher::tool("Bash")),
27//!     config: HookConfig::default(),
28//! });
29//!
30//! // Fire hook and get result
31//! let result = engine.fire(HookEvent::PreToolUse { ... }).await;
32//! match result {
33//!     HookResult::Continue(None) => { /* proceed */ }
34//!     HookResult::Continue(Some(modified)) => { /* proceed with modified data */ }
35//!     HookResult::Block(reason) => { /* stop execution */ }
36//! }
37//! ```
38
39mod engine;
40mod events;
41mod matcher;
42
43pub use engine::{Hook, HookConfig, HookEngine, HookExecutor, HookHandler, HookResult};
44pub use events::{
45    ConfirmationType, ErrorType, GenerateEndEvent, GenerateStartEvent, HookEvent, HookEventType,
46    IntentDetectionEvent, OnConfirmationEvent, OnErrorEvent, OnRateLimitEvent, OnSuccessEvent,
47    PlanningStrategy, PostContextPerceptionEvent, PostMemoryRecallEvent, PostPlanningEvent,
48    PostReasoningEvent, PostResponseEvent, PostToolUseEvent, PreContextPerceptionEvent,
49    PreMemoryRecallEvent, PrePlanningEvent, PrePromptEvent, PreReasoningEvent, PreToolUseEvent,
50    RateLimitType, ReasoningType, SessionEndEvent, SessionStartEvent, SkillLoadEvent,
51    SkillUnloadEvent, TokenUsageInfo, ToolCallInfo, ToolResultData,
52};
53pub use matcher::HookMatcher;
54
55/// Hook response action from SDK
56#[derive(Debug, Clone, PartialEq)]
57pub enum HookAction {
58    /// Proceed with execution (optionally with modifications)
59    Continue,
60    /// Block the operation
61    Block,
62    /// Retry after a delay
63    Retry,
64    /// Skip remaining hooks but continue execution
65    Skip,
66}
67
68/// Response from a hook handler
69#[derive(Debug, Clone)]
70pub struct HookResponse {
71    /// The hook ID this response is for
72    pub hook_id: String,
73    /// Action to take
74    pub action: HookAction,
75    /// Reason for blocking (if action is Block)
76    pub reason: Option<String>,
77    /// Modified data (if action is Continue with modifications)
78    pub modified: Option<serde_json::Value>,
79    /// Retry delay in milliseconds (if action is Retry)
80    pub retry_delay_ms: Option<u64>,
81}
82
83impl HookResponse {
84    /// Create a continue response
85    pub fn continue_() -> Self {
86        Self {
87            hook_id: String::new(),
88            action: HookAction::Continue,
89            reason: None,
90            modified: None,
91            retry_delay_ms: None,
92        }
93    }
94
95    /// Create a continue response with modifications
96    pub fn continue_with(modified: serde_json::Value) -> Self {
97        Self {
98            hook_id: String::new(),
99            action: HookAction::Continue,
100            reason: None,
101            modified: Some(modified),
102            retry_delay_ms: None,
103        }
104    }
105
106    /// Create a block response
107    pub fn block(reason: impl Into<String>) -> Self {
108        Self {
109            hook_id: String::new(),
110            action: HookAction::Block,
111            reason: Some(reason.into()),
112            modified: None,
113            retry_delay_ms: None,
114        }
115    }
116
117    /// Create a retry response
118    pub fn retry(delay_ms: u64) -> Self {
119        Self {
120            hook_id: String::new(),
121            action: HookAction::Retry,
122            reason: None,
123            modified: None,
124            retry_delay_ms: Some(delay_ms),
125        }
126    }
127
128    /// Create a skip response
129    pub fn skip() -> Self {
130        Self {
131            hook_id: String::new(),
132            action: HookAction::Skip,
133            reason: None,
134            modified: None,
135            retry_delay_ms: None,
136        }
137    }
138
139    /// Set the hook ID
140    pub fn with_hook_id(mut self, id: impl Into<String>) -> Self {
141        self.hook_id = id.into();
142        self
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_hook_response_continue() {
152        let response = HookResponse::continue_();
153        assert_eq!(response.action, HookAction::Continue);
154        assert!(response.reason.is_none());
155        assert!(response.modified.is_none());
156    }
157
158    #[test]
159    fn test_hook_response_continue_with_modified() {
160        let modified = serde_json::json!({"timeout": 5000});
161        let response = HookResponse::continue_with(modified.clone());
162        assert_eq!(response.action, HookAction::Continue);
163        assert_eq!(response.modified, Some(modified));
164    }
165
166    #[test]
167    fn test_hook_response_block() {
168        let response = HookResponse::block("Dangerous command");
169        assert_eq!(response.action, HookAction::Block);
170        assert_eq!(response.reason, Some("Dangerous command".to_string()));
171    }
172
173    #[test]
174    fn test_hook_response_retry() {
175        let response = HookResponse::retry(1000);
176        assert_eq!(response.action, HookAction::Retry);
177        assert_eq!(response.retry_delay_ms, Some(1000));
178    }
179
180    #[test]
181    fn test_hook_response_skip() {
182        let response = HookResponse::skip();
183        assert_eq!(response.action, HookAction::Skip);
184    }
185
186    #[test]
187    fn test_hook_response_with_hook_id() {
188        let response = HookResponse::continue_().with_hook_id("hook-123");
189        assert_eq!(response.hook_id, "hook-123");
190    }
191}