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::{
44    Hook, HookConfig, HookEngine, HookExecutor, HookHandler, HookOutcome, HookResult,
45};
46pub use events::{
47    ConfirmationType, ErrorType, GenerateEndEvent, GenerateStartEvent, HookEvent, HookEventType,
48    IntentDetectionEvent, OnConfirmationEvent, OnErrorEvent, OnRateLimitEvent, OnSuccessEvent,
49    PermissionRequestEvent, PlanningStrategy, PostCompactEvent, PostContextPerceptionEvent,
50    PostMemoryRecallEvent, PostPlanningEvent, PostReasoningEvent, PostResponseEvent,
51    PostToolUseEvent, PreCompactEvent, PreContextPerceptionEvent, PreMemoryRecallEvent,
52    PrePlanningEvent, PrePromptEvent, PreReasoningEvent, PreToolUseEvent, RateLimitType,
53    ReasoningType, SessionEndEvent, SessionStartEvent, SkillLoadEvent, SkillUnloadEvent,
54    TokenUsageInfo, ToolCallInfo, ToolResultData,
55};
56pub use matcher::HookMatcher;
57
58/// Hook response action from SDK
59#[derive(Debug, Clone, PartialEq)]
60pub enum HookAction {
61    /// Proceed with execution (optionally with modifications)
62    Continue,
63    /// Block the operation
64    Block,
65    /// Retry after a delay
66    Retry,
67    /// Skip remaining hooks but continue execution
68    Skip,
69}
70
71/// Response from a hook handler
72#[derive(Debug, Clone)]
73pub struct HookResponse {
74    /// The hook ID this response is for
75    pub hook_id: String,
76    /// Action to take
77    pub action: HookAction,
78    /// Reason for blocking or retrying (if action is Block or Retry)
79    pub reason: Option<String>,
80    /// Modified data (if action is Continue with modifications)
81    pub modified: Option<serde_json::Value>,
82    /// Retry delay in milliseconds (if action is Retry)
83    pub retry_delay_ms: Option<u64>,
84}
85
86impl HookResponse {
87    /// Create a continue response
88    pub fn continue_() -> Self {
89        Self {
90            hook_id: String::new(),
91            action: HookAction::Continue,
92            reason: None,
93            modified: None,
94            retry_delay_ms: None,
95        }
96    }
97
98    /// Create a continue response with modifications
99    pub fn continue_with(modified: serde_json::Value) -> Self {
100        Self {
101            hook_id: String::new(),
102            action: HookAction::Continue,
103            reason: None,
104            modified: Some(modified),
105            retry_delay_ms: None,
106        }
107    }
108
109    /// Create a block response
110    pub fn block(reason: impl Into<String>) -> Self {
111        Self {
112            hook_id: String::new(),
113            action: HookAction::Block,
114            reason: Some(reason.into()),
115            modified: None,
116            retry_delay_ms: None,
117        }
118    }
119
120    /// Create a retry response
121    pub fn retry(delay_ms: u64) -> Self {
122        Self {
123            hook_id: String::new(),
124            action: HookAction::Retry,
125            reason: None,
126            modified: None,
127            retry_delay_ms: Some(delay_ms),
128        }
129    }
130
131    /// Create a retry response with actionable context.
132    pub fn retry_with_reason(reason: impl Into<String>, delay_ms: u64) -> Self {
133        Self {
134            hook_id: String::new(),
135            action: HookAction::Retry,
136            reason: Some(reason.into()),
137            modified: None,
138            retry_delay_ms: Some(delay_ms),
139        }
140    }
141
142    /// Create a skip response
143    pub fn skip() -> Self {
144        Self {
145            hook_id: String::new(),
146            action: HookAction::Skip,
147            reason: None,
148            modified: None,
149            retry_delay_ms: None,
150        }
151    }
152
153    /// Set the hook ID
154    pub fn with_hook_id(mut self, id: impl Into<String>) -> Self {
155        self.hook_id = id.into();
156        self
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_hook_response_continue() {
166        let response = HookResponse::continue_();
167        assert_eq!(response.action, HookAction::Continue);
168        assert!(response.reason.is_none());
169        assert!(response.modified.is_none());
170    }
171
172    #[test]
173    fn test_hook_response_continue_with_modified() {
174        let modified = serde_json::json!({"timeout": 5000});
175        let response = HookResponse::continue_with(modified.clone());
176        assert_eq!(response.action, HookAction::Continue);
177        assert_eq!(response.modified, Some(modified));
178    }
179
180    #[test]
181    fn test_hook_response_block() {
182        let response = HookResponse::block("Dangerous command");
183        assert_eq!(response.action, HookAction::Block);
184        assert_eq!(response.reason, Some("Dangerous command".to_string()));
185    }
186
187    #[test]
188    fn test_hook_response_retry() {
189        let response = HookResponse::retry(1000);
190        assert_eq!(response.action, HookAction::Retry);
191        assert_eq!(response.retry_delay_ms, Some(1000));
192    }
193
194    #[test]
195    fn test_hook_response_retry_with_reason() {
196        let response = HookResponse::retry_with_reason("temporary outage", 750);
197        assert_eq!(response.action, HookAction::Retry);
198        assert_eq!(response.reason.as_deref(), Some("temporary outage"));
199        assert_eq!(response.retry_delay_ms, Some(750));
200    }
201
202    #[test]
203    fn test_hook_response_skip() {
204        let response = HookResponse::skip();
205        assert_eq!(response.action, HookAction::Skip);
206    }
207
208    #[test]
209    fn test_hook_response_with_hook_id() {
210        let response = HookResponse::continue_().with_hook_id("hook-123");
211        assert_eq!(response.hook_id, "hook-123");
212    }
213}