1mod binding;
40mod contract;
41mod engine;
42mod events;
43mod matcher;
44
45pub use binding::HookBinding;
46pub use contract::{Hook, HookConfig, HookExecutor, HookHandler, HookOutcome, HookResult};
47pub use engine::HookEngine;
48pub(crate) use engine::{HookEngineSnapshotError, HookTaskDispatcher, HookTaskFuture};
49pub use events::{
50 ConfirmationType, ErrorType, GenerateEndEvent, GenerateStartEvent, HookEvent, HookEventType,
51 IntentDetectionEvent, OnConfirmationEvent, OnErrorEvent, OnRateLimitEvent, OnSuccessEvent,
52 PermissionRequestEvent, PlanningStrategy, PostCompactEvent, PostContextPerceptionEvent,
53 PostMemoryRecallEvent, PostPlanningEvent, PostReasoningEvent, PostResponseEvent,
54 PostToolUseEvent, PreCompactEvent, PreContextPerceptionEvent, PreMemoryRecallEvent,
55 PrePlanningEvent, PrePromptEvent, PreReasoningEvent, PreToolUseEvent, RateLimitType,
56 ReasoningType, SessionEndEvent, SessionStartEvent, SkillLoadEvent, SkillUnloadEvent,
57 TokenUsageInfo, ToolCallInfo, ToolResultData,
58};
59pub use matcher::HookMatcher;
60
61#[derive(Debug, Clone, PartialEq)]
63pub enum HookAction {
64 Continue,
66 Block,
68 Retry,
70 Skip,
72}
73
74#[derive(Debug, Clone)]
76pub struct HookResponse {
77 pub hook_id: String,
79 pub action: HookAction,
81 pub reason: Option<String>,
83 pub modified: Option<serde_json::Value>,
85 pub retry_delay_ms: Option<u64>,
87}
88
89impl HookResponse {
90 pub fn continue_() -> Self {
92 Self {
93 hook_id: String::new(),
94 action: HookAction::Continue,
95 reason: None,
96 modified: None,
97 retry_delay_ms: None,
98 }
99 }
100
101 pub fn continue_with(modified: serde_json::Value) -> Self {
103 Self {
104 hook_id: String::new(),
105 action: HookAction::Continue,
106 reason: None,
107 modified: Some(modified),
108 retry_delay_ms: None,
109 }
110 }
111
112 pub fn block(reason: impl Into<String>) -> Self {
114 Self {
115 hook_id: String::new(),
116 action: HookAction::Block,
117 reason: Some(reason.into()),
118 modified: None,
119 retry_delay_ms: None,
120 }
121 }
122
123 pub fn retry(delay_ms: u64) -> Self {
125 Self {
126 hook_id: String::new(),
127 action: HookAction::Retry,
128 reason: None,
129 modified: None,
130 retry_delay_ms: Some(delay_ms),
131 }
132 }
133
134 pub fn retry_with_reason(reason: impl Into<String>, delay_ms: u64) -> Self {
136 Self {
137 hook_id: String::new(),
138 action: HookAction::Retry,
139 reason: Some(reason.into()),
140 modified: None,
141 retry_delay_ms: Some(delay_ms),
142 }
143 }
144
145 pub fn skip() -> Self {
147 Self {
148 hook_id: String::new(),
149 action: HookAction::Skip,
150 reason: None,
151 modified: None,
152 retry_delay_ms: None,
153 }
154 }
155
156 pub fn with_hook_id(mut self, id: impl Into<String>) -> Self {
158 self.hook_id = id.into();
159 self
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_hook_response_continue() {
169 let response = HookResponse::continue_();
170 assert_eq!(response.action, HookAction::Continue);
171 assert!(response.reason.is_none());
172 assert!(response.modified.is_none());
173 }
174
175 #[test]
176 fn test_hook_response_continue_with_modified() {
177 let modified = serde_json::json!({"timeout": 5000});
178 let response = HookResponse::continue_with(modified.clone());
179 assert_eq!(response.action, HookAction::Continue);
180 assert_eq!(response.modified, Some(modified));
181 }
182
183 #[test]
184 fn test_hook_response_block() {
185 let response = HookResponse::block("Dangerous command");
186 assert_eq!(response.action, HookAction::Block);
187 assert_eq!(response.reason, Some("Dangerous command".to_string()));
188 }
189
190 #[test]
191 fn test_hook_response_retry() {
192 let response = HookResponse::retry(1000);
193 assert_eq!(response.action, HookAction::Retry);
194 assert_eq!(response.retry_delay_ms, Some(1000));
195 }
196
197 #[test]
198 fn test_hook_response_retry_with_reason() {
199 let response = HookResponse::retry_with_reason("temporary outage", 750);
200 assert_eq!(response.action, HookAction::Retry);
201 assert_eq!(response.reason.as_deref(), Some("temporary outage"));
202 assert_eq!(response.retry_delay_ms, Some(750));
203 }
204
205 #[test]
206 fn test_hook_response_skip() {
207 let response = HookResponse::skip();
208 assert_eq!(response.action, HookAction::Skip);
209 }
210
211 #[test]
212 fn test_hook_response_with_hook_id() {
213 let response = HookResponse::continue_().with_hook_id("hook-123");
214 assert_eq!(response.hook_id, "hook-123");
215 }
216}