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