Skip to main content

a3s_code_core/hooks/
contract.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5use super::{HookEvent, HookEventType, HookMatcher, HookResponse};
6
7/// Hook configuration.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct HookConfig {
10    /// Priority (lower values = higher priority).
11    #[serde(default = "default_priority")]
12    pub priority: i32,
13
14    /// Timeout in milliseconds.
15    #[serde(default = "default_timeout")]
16    pub timeout_ms: u64,
17
18    /// Whether to execute observational hooks asynchronously (fire-and-forget).
19    /// Gating hooks always wait for a decision before protected work starts.
20    #[serde(default)]
21    pub async_execution: bool,
22
23    /// Maximum retry attempts.
24    #[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/// Hook definition.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Hook {
50    /// Unique hook identifier.
51    pub id: String,
52
53    /// Event type that triggers this hook.
54    pub event_type: HookEventType,
55
56    /// Event matcher (optional, `None` matches all events).
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub matcher: Option<HookMatcher>,
59
60    /// Hook configuration.
61    #[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/// Hook execution result.
96#[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/// Rich Hook execution outcome used by governance-aware callers.
146#[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    /// SDK bridges override this method so callback infrastructure failures
195    /// reach the engine instead of being converted to `Continue`.
196    fn try_handle(&self, event: &HookEvent) -> Result<HookResponse, String> {
197        Ok(self.handle(event))
198    }
199}
200
201/// Execution seam for in-process, SDK, and host-provided Hook runtimes.
202#[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    /// Dispatch an observational event without delaying protected work.
211    /// Run-scoped executors override this to register the work with their
212    /// capability supervisor before returning.
213    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}