a3s_code_core/hooks/
contract.rs1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5use super::{HookEvent, HookEventType, HookMatcher, HookResponse};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct HookConfig {
10 #[serde(default = "default_priority")]
12 pub priority: i32,
13
14 #[serde(default = "default_timeout")]
16 pub timeout_ms: u64,
17
18 #[serde(default)]
21 pub async_execution: bool,
22
23 #[serde(default)]
25 pub max_retries: u32,
26}
27
28fn default_priority() -> i32 {
29 100
30}
31
32fn default_timeout() -> u64 {
33 30000
34}
35
36impl Default for HookConfig {
37 fn default() -> Self {
38 Self {
39 priority: default_priority(),
40 timeout_ms: default_timeout(),
41 async_execution: false,
42 max_retries: 0,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Hook {
50 pub id: String,
52
53 pub event_type: HookEventType,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub matcher: Option<HookMatcher>,
59
60 #[serde(default)]
62 pub config: HookConfig,
63}
64
65impl Hook {
66 pub fn new(id: impl Into<String>, event_type: HookEventType) -> Self {
67 Self {
68 id: id.into(),
69 event_type,
70 matcher: None,
71 config: HookConfig::default(),
72 }
73 }
74
75 pub fn with_matcher(mut self, matcher: HookMatcher) -> Self {
76 self.matcher = Some(matcher);
77 self
78 }
79
80 pub fn with_config(mut self, config: HookConfig) -> Self {
81 self.config = config;
82 self
83 }
84
85 pub fn matches(&self, event: &HookEvent) -> bool {
86 if event.event_type() != self.event_type {
87 return false;
88 }
89 self.matcher
90 .as_ref()
91 .is_none_or(|matcher| matcher.matches(event))
92 }
93}
94
95#[derive(Debug, Clone)]
97pub enum HookResult {
98 Continue(Option<serde_json::Value>),
99 Block(String),
100 Retry(u64),
101 Skip,
102 Escalate {
103 reason: String,
104 target: Option<String>,
105 },
106}
107
108impl HookResult {
109 pub fn continue_() -> Self {
110 Self::Continue(None)
111 }
112
113 pub fn continue_with(modified: serde_json::Value) -> Self {
114 Self::Continue(Some(modified))
115 }
116
117 pub fn block(reason: impl Into<String>) -> Self {
118 Self::Block(reason.into())
119 }
120
121 pub fn retry(delay_ms: u64) -> Self {
122 Self::Retry(delay_ms)
123 }
124
125 pub fn skip() -> Self {
126 Self::Skip
127 }
128
129 pub fn escalate(reason: impl Into<String>, target: Option<String>) -> Self {
130 Self::Escalate {
131 reason: reason.into(),
132 target,
133 }
134 }
135
136 pub fn is_continue(&self) -> bool {
137 matches!(self, Self::Continue(_))
138 }
139
140 pub fn is_block(&self) -> bool {
141 matches!(self, Self::Block(_))
142 }
143}
144
145#[derive(Debug, Clone)]
147#[non_exhaustive]
148pub enum HookOutcome {
149 Continue(Option<serde_json::Value>),
150 Block {
151 reason: String,
152 },
153 Retry {
154 reason: String,
155 retry_after_ms: u64,
156 },
157 Skip,
158 Escalate {
159 reason: String,
160 target: Option<String>,
161 },
162}
163
164impl From<HookResult> for HookOutcome {
165 fn from(result: HookResult) -> Self {
166 match result {
167 HookResult::Continue(modified) => Self::Continue(modified),
168 HookResult::Block(reason) => Self::Block { reason },
169 HookResult::Retry(retry_after_ms) => Self::Retry {
170 reason: "Hook requested a retry".to_string(),
171 retry_after_ms,
172 },
173 HookResult::Skip => Self::Skip,
174 HookResult::Escalate { reason, target } => Self::Escalate { reason, target },
175 }
176 }
177}
178
179impl From<HookOutcome> for HookResult {
180 fn from(outcome: HookOutcome) -> Self {
181 match outcome {
182 HookOutcome::Continue(modified) => Self::Continue(modified),
183 HookOutcome::Block { reason } => Self::Block(reason),
184 HookOutcome::Retry { retry_after_ms, .. } => Self::Retry(retry_after_ms),
185 HookOutcome::Skip => Self::Skip,
186 HookOutcome::Escalate { reason, target } => Self::Escalate { reason, target },
187 }
188 }
189}
190
191pub trait HookHandler: Send + Sync {
192 fn handle(&self, event: &HookEvent) -> HookResponse;
193
194 fn try_handle(&self, event: &HookEvent) -> Result<HookResponse, String> {
197 Ok(self.handle(event))
198 }
199}
200
201#[async_trait::async_trait]
203pub trait HookExecutor: Send + Sync + std::fmt::Debug + 'static {
204 async fn fire(&self, event: &HookEvent) -> HookResult;
205
206 async fn fire_outcome(&self, event: &HookEvent) -> HookOutcome {
207 self.fire(event).await.into()
208 }
209
210 fn dispatch_observational(self: Arc<Self>, event: HookEvent) {
214 tokio::spawn(async move {
215 let _ = self.fire(&event).await;
216 });
217 }
218
219 async fn record_agent_event(
220 &self,
221 _event: &crate::agent::AgentEvent,
222 _run_id: &str,
223 _session_id: &str,
224 ) {
225 }
226
227 async fn record_run_cancelled(&self, _run_id: &str, _session_id: &str, _reason: Option<&str>) {}
228}