Skip to main content

talos_plugin/
event.rs

1//! Hook event types.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Duration;
5
6use talos_core::message::{Message, ToolCall};
7use talos_core::provider::ProviderError;
8use talos_core::tool::ToolResult;
9use talos_permission::PermissionDecision;
10
11static NEXT_TURN_ID: AtomicU64 = AtomicU64::new(1);
12
13/// A stable identifier for a single agent turn.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct TurnId(pub u64);
16
17impl TurnId {
18    /// Creates a new unique turn identifier.
19    #[must_use]
20    pub fn new() -> Self {
21        Self(NEXT_TURN_ID.fetch_add(1, Ordering::Relaxed))
22    }
23}
24
25impl Default for TurnId {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31/// Final status recorded for a completed turn.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TurnStatus {
34    /// The turn completed successfully.
35    Success,
36    /// The provider failed before the turn could complete.
37    ProviderError,
38    /// The provider emitted an unexpected event sequence.
39    UnexpectedEvent,
40    /// The turn exceeded its tool-call budget.
41    BudgetExceeded,
42    /// The turn was terminated due to doom-loop detection.
43    DoomLoopDetected,
44    /// The turn was denied by a hook.
45    Denied,
46}
47
48/// Stop reason observed from the provider stream.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum TurnEndReason {
51    /// The assistant finished naturally.
52    EndTurn,
53    /// The assistant requested tool use.
54    ToolUse,
55    /// The provider hit its maximum token limit.
56    MaxTokens,
57}
58
59/// Budget category exceeded by the runtime.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum BudgetKind {
62    /// The turn exceeded the tool-call count limit.
63    ToolCalls,
64}
65
66/// Owned observation of a completed tool result.
67#[derive(Debug, Clone)]
68pub struct ToolObservation {
69    /// The tool call that produced the result.
70    pub call: ToolCall,
71    /// The observed tool result.
72    pub result: ToolResult,
73}
74
75/// Discriminant for hook event subscription and dispatch.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum HookEventKind {
78    /// `HookEvent::TurnStart`.
79    TurnStart,
80    /// `HookEvent::OnSystemPromptBuilt`.
81    OnSystemPromptBuilt,
82    /// `HookEvent::BeforeProviderCall`.
83    BeforeProviderCall,
84    /// `HookEvent::AfterProviderCall`.
85    AfterProviderCall,
86    /// `HookEvent::OnTextDelta`.
87    OnTextDelta,
88    /// `HookEvent::OnToolCallProposed`.
89    OnToolCallProposed,
90    /// `HookEvent::BeforeToolBatch`.
91    BeforeToolBatch,
92    /// `HookEvent::BeforePermissionCheck`.
93    BeforePermissionCheck,
94    /// `HookEvent::AfterPermissionCheck`.
95    AfterPermissionCheck,
96    /// `HookEvent::BeforeBashSandboxExec`.
97    BeforeBashSandboxExec,
98    /// `HookEvent::AfterBashSandboxExec`.
99    AfterBashSandboxExec,
100    /// `HookEvent::BeforeToolCall`.
101    BeforeToolCall,
102    /// `HookEvent::AfterToolCall`.
103    AfterToolCall,
104    /// `HookEvent::OnToolResultObserved`.
105    OnToolResultObserved,
106    /// `HookEvent::AfterToolBatch`.
107    AfterToolBatch,
108    /// `HookEvent::OnDoomLoopDetected`.
109    OnDoomLoopDetected,
110    /// `HookEvent::OnBudgetExceeded`.
111    OnBudgetExceeded,
112    /// `HookEvent::OnProviderError`.
113    OnProviderError,
114    /// `HookEvent::OnTurnEnd`.
115    OnTurnEnd,
116    /// `HookEvent::TurnComplete`.
117    TurnComplete,
118}
119
120impl std::fmt::Display for HookEventKind {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{self:?}")
123    }
124}
125
126/// All currently defined hook event kinds.
127pub const ALL_HOOK_EVENT_KINDS: [HookEventKind; 20] = [
128    HookEventKind::TurnStart,
129    HookEventKind::OnSystemPromptBuilt,
130    HookEventKind::BeforeProviderCall,
131    HookEventKind::AfterProviderCall,
132    HookEventKind::OnTextDelta,
133    HookEventKind::OnToolCallProposed,
134    HookEventKind::BeforeToolBatch,
135    HookEventKind::BeforePermissionCheck,
136    HookEventKind::AfterPermissionCheck,
137    HookEventKind::BeforeBashSandboxExec,
138    HookEventKind::AfterBashSandboxExec,
139    HookEventKind::BeforeToolCall,
140    HookEventKind::AfterToolCall,
141    HookEventKind::OnToolResultObserved,
142    HookEventKind::AfterToolBatch,
143    HookEventKind::OnDoomLoopDetected,
144    HookEventKind::OnBudgetExceeded,
145    HookEventKind::OnProviderError,
146    HookEventKind::OnTurnEnd,
147    HookEventKind::TurnComplete,
148];
149
150/// A lifecycle event emitted by the Talos runtime.
151#[derive(Debug)]
152#[non_exhaustive]
153pub enum HookEvent<'a> {
154    /// The turn has started.
155    TurnStart {
156        /// Current turn identifier.
157        turn_id: TurnId,
158    },
159    /// The system prompt has been assembled.
160    OnSystemPromptBuilt {
161        /// Final prompt text.
162        prompt: &'a str,
163    },
164    /// Immediately before invoking the provider.
165    BeforeProviderCall {
166        /// Messages sent to the provider.
167        messages: &'a [Message],
168    },
169    /// After the provider stream has completed.
170    AfterProviderCall {
171        /// Input tokens consumed.
172        tokens_in: u32,
173        /// Output tokens produced.
174        tokens_out: u32,
175    },
176    /// A text delta was observed.
177    OnTextDelta {
178        /// Delta text.
179        text: &'a str,
180    },
181    /// A tool call was proposed by the model.
182    OnToolCallProposed {
183        /// Proposed tool call.
184        call: &'a ToolCall,
185    },
186    /// Immediately before executing a batch of tool calls.
187    BeforeToolBatch {
188        /// Tool calls to execute.
189        calls: &'a [ToolCall],
190    },
191    /// Immediately before evaluating permissions for a tool call.
192    BeforePermissionCheck {
193        /// Tool call being evaluated.
194        call: &'a ToolCall,
195    },
196    /// Immediately after evaluating permissions for a tool call.
197    AfterPermissionCheck {
198        /// Tool call being evaluated.
199        call: &'a ToolCall,
200        /// Permission decision returned by the engine.
201        decision: PermissionDecision,
202    },
203    /// Immediately before executing a bash command in the sandbox.
204    BeforeBashSandboxExec {
205        /// Bash command string.
206        command: &'a str,
207    },
208    /// Immediately after sandboxed bash execution completes.
209    AfterBashSandboxExec {
210        /// Process exit code.
211        exit: i32,
212        /// Total execution duration.
213        duration: Duration,
214    },
215    /// Immediately before invoking a tool.
216    BeforeToolCall {
217        /// Tool call being executed.
218        call: &'a ToolCall,
219    },
220    /// Immediately after invoking a tool.
221    AfterToolCall {
222        /// Tool call that was executed.
223        call: &'a ToolCall,
224        /// Tool result returned by the tool.
225        result: &'a ToolResult,
226    },
227    /// A tool result was observed and added back into the conversation.
228    OnToolResultObserved {
229        /// Owned observation payload.
230        observation: &'a ToolObservation,
231    },
232    /// After a batch of tool calls has completed.
233    AfterToolBatch {
234        /// Tool results in input order.
235        results: &'a [ToolResult],
236    },
237    /// Doom-loop detection fired.
238    OnDoomLoopDetected {
239        /// Doom-loop signature string.
240        signature: &'a str,
241    },
242    /// A runtime budget was exceeded.
243    OnBudgetExceeded {
244        /// Budget category.
245        kind: BudgetKind,
246        /// Amount used.
247        used: u64,
248        /// Configured limit.
249        limit: u64,
250    },
251    /// A provider error occurred.
252    OnProviderError {
253        /// Provider error instance.
254        error: &'a ProviderError,
255    },
256    /// The provider signaled turn end.
257    OnTurnEnd {
258        /// Turn-end reason.
259        reason: TurnEndReason,
260    },
261    /// The turn completed.
262    TurnComplete {
263        /// Current turn identifier.
264        turn_id: TurnId,
265        /// Final turn status.
266        status: TurnStatus,
267    },
268}
269
270impl HookEvent<'_> {
271    /// Returns the discriminant used for subscription and pre-filtering.
272    #[must_use]
273    pub fn kind(&self) -> HookEventKind {
274        match self {
275            Self::TurnStart { .. } => HookEventKind::TurnStart,
276            Self::OnSystemPromptBuilt { .. } => HookEventKind::OnSystemPromptBuilt,
277            Self::BeforeProviderCall { .. } => HookEventKind::BeforeProviderCall,
278            Self::AfterProviderCall { .. } => HookEventKind::AfterProviderCall,
279            Self::OnTextDelta { .. } => HookEventKind::OnTextDelta,
280            Self::OnToolCallProposed { .. } => HookEventKind::OnToolCallProposed,
281            Self::BeforeToolBatch { .. } => HookEventKind::BeforeToolBatch,
282            Self::BeforePermissionCheck { .. } => HookEventKind::BeforePermissionCheck,
283            Self::AfterPermissionCheck { .. } => HookEventKind::AfterPermissionCheck,
284            Self::BeforeBashSandboxExec { .. } => HookEventKind::BeforeBashSandboxExec,
285            Self::AfterBashSandboxExec { .. } => HookEventKind::AfterBashSandboxExec,
286            Self::BeforeToolCall { .. } => HookEventKind::BeforeToolCall,
287            Self::AfterToolCall { .. } => HookEventKind::AfterToolCall,
288            Self::OnToolResultObserved { .. } => HookEventKind::OnToolResultObserved,
289            Self::AfterToolBatch { .. } => HookEventKind::AfterToolBatch,
290            Self::OnDoomLoopDetected { .. } => HookEventKind::OnDoomLoopDetected,
291            Self::OnBudgetExceeded { .. } => HookEventKind::OnBudgetExceeded,
292            Self::OnProviderError { .. } => HookEventKind::OnProviderError,
293            Self::OnTurnEnd { .. } => HookEventKind::OnTurnEnd,
294            Self::TurnComplete { .. } => HookEventKind::TurnComplete,
295        }
296    }
297
298    /// Returns whether this event is inside the permission read-only boundary.
299    #[must_use]
300    pub fn is_permission_boundary(&self) -> bool {
301        matches!(
302            self,
303            Self::OnToolCallProposed { .. }
304                | Self::BeforePermissionCheck { .. }
305                | Self::AfterPermissionCheck { .. }
306        )
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn kind_round_trip() {
316        let turn_id = TurnId::new();
317        let event = HookEvent::TurnStart { turn_id };
318        assert_eq!(event.kind(), HookEventKind::TurnStart);
319        assert_eq!(ALL_HOOK_EVENT_KINDS.len(), 20);
320    }
321}