Skip to main content

mixtape_core/
events.rs

1use std::time::{Duration, Instant};
2
3use serde_json::Value;
4
5use crate::permission::Scope;
6use crate::tool::ToolResult;
7use crate::types::StopReason;
8
9/// Events emitted during agent execution
10///
11/// These events allow observers to track agent lifecycle, model calls,
12/// and tool executions in real-time.
13#[derive(Debug, Clone)]
14pub enum AgentEvent {
15    // ===== Agent Lifecycle =====
16    /// Agent.run() started
17    RunStarted {
18        /// User input message
19        input: String,
20        /// Timestamp
21        timestamp: Instant,
22    },
23
24    /// Agent.run() completed
25    RunCompleted {
26        /// Final response to user
27        output: String,
28        /// Total execution duration
29        duration: Duration,
30    },
31
32    /// Agent.run() failed with error
33    RunFailed {
34        /// Error message
35        error: String,
36        /// How long before failure
37        duration: Duration,
38    },
39
40    // ===== Model API Lifecycle =====
41    /// Model API call started
42    ModelCallStarted {
43        /// Messages being sent to model
44        message_count: usize,
45        /// Number of tools available to model
46        tool_count: usize,
47        /// Timestamp
48        timestamp: Instant,
49    },
50
51    /// Model streaming a token (only if streaming enabled)
52    ModelCallStreaming {
53        /// Incremental text delta
54        delta: String,
55        /// Accumulated length so far
56        accumulated_length: usize,
57    },
58
59    /// Model API call completed
60    ModelCallCompleted {
61        /// Response content
62        response_content: String,
63        /// Token usage statistics
64        tokens: Option<TokenUsage>,
65        /// API call duration
66        duration: Duration,
67        /// Stop reason from model
68        stop_reason: Option<StopReason>,
69    },
70
71    // ===== Tool Lifecycle =====
72    /// Model requested a tool (fires exactly once per tool use)
73    ToolRequested {
74        /// Unique ID for this tool use
75        tool_use_id: String,
76        /// Tool name
77        name: String,
78        /// Input parameters
79        input: Value,
80    },
81
82    /// Tool execution actually starting (after permission granted)
83    ToolExecuting {
84        /// Unique ID for this tool use
85        tool_use_id: String,
86        /// Tool name
87        name: String,
88    },
89
90    /// Tool execution completed successfully
91    ToolCompleted {
92        /// Matching ID from ToolRequested
93        tool_use_id: String,
94        /// Tool name
95        name: String,
96        /// Tool output
97        output: ToolResult,
98        /// Execution duration
99        duration: Duration,
100    },
101
102    /// Tool execution failed
103    ToolFailed {
104        /// Matching ID from ToolRequested
105        tool_use_id: String,
106        /// Tool name
107        name: String,
108        /// Error message
109        error: String,
110        /// How long before failure
111        duration: Duration,
112    },
113
114    // ===== Permission Events =====
115    /// Tool execution requires permission
116    PermissionRequired {
117        /// Unique ID for this permission request
118        proposal_id: String,
119        /// Tool name
120        tool_name: String,
121        /// Tool input parameters
122        params: Value,
123        /// Hash of parameters (for creating exact-match grants)
124        params_hash: String,
125    },
126
127    /// Permission granted (auto-approved or user-approved)
128    PermissionGranted {
129        /// Tool use ID
130        tool_use_id: String,
131        /// Tool name
132        tool_name: String,
133        /// The scope of the grant (None if one-time approval)
134        scope: Option<Scope>,
135    },
136
137    /// Permission denied
138    PermissionDenied {
139        /// Tool use ID
140        tool_use_id: String,
141        /// Tool name
142        tool_name: String,
143        /// Reason for denial
144        reason: String,
145    },
146
147    // ===== Session Events =====
148    #[cfg(feature = "session")]
149    /// Session resumed from storage
150    SessionResumed {
151        /// Session ID
152        session_id: String,
153        /// Number of prior messages in session
154        message_count: usize,
155        /// When session was created
156        created_at: chrono::DateTime<chrono::Utc>,
157    },
158
159    #[cfg(feature = "session")]
160    /// Session saved to storage
161    SessionSaved {
162        /// Session ID
163        session_id: String,
164        /// Total messages in session now
165        message_count: usize,
166    },
167}
168
169/// Token usage statistics from model
170#[derive(Debug, Clone, Copy)]
171pub struct TokenUsage {
172    pub input_tokens: usize,
173    pub output_tokens: usize,
174}
175
176impl TokenUsage {
177    pub fn total(&self) -> usize {
178        self.input_tokens + self.output_tokens
179    }
180}
181
182/// Hook for observing agent events
183///
184/// Implement this trait to receive notifications about agent execution.
185///
186/// # Example
187/// ```
188/// use mixtape_core::events::{AgentEvent, AgentHook};
189///
190/// struct Logger;
191///
192/// impl AgentHook for Logger {
193///     fn on_event(&self, event: &AgentEvent) {
194///         match event {
195///             AgentEvent::RunStarted { input, .. } => {
196///                 println!("Starting: {}", input);
197///             }
198///             AgentEvent::ToolRequested { name, .. } => {
199///                 println!("Tool requested: {}", name);
200///             }
201///             _ => {}
202///         }
203///     }
204/// }
205/// ```
206pub trait AgentHook: Send + Sync {
207    /// Called when an event occurs
208    fn on_event(&self, event: &AgentEvent);
209}
210
211/// Blanket implementation for closures
212impl<F> AgentHook for F
213where
214    F: Fn(&AgentEvent) + Send + Sync,
215{
216    fn on_event(&self, event: &AgentEvent) {
217        self(event)
218    }
219}
220
221/// Unique identifier for a registered hook.
222///
223/// Used to remove hooks via [`crate::Agent::remove_hook`].
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
225pub struct HookId(pub(crate) u64);
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_token_usage_total() {
233        let cases = [
234            (100, 50, 150),
235            (0, 0, 0),
236            (1000, 2000, 3000),
237            (1, 0, 1),
238            (0, 1, 1),
239            (usize::MAX / 2, usize::MAX / 2, usize::MAX - 1),
240        ];
241
242        for (input, output, expected) in cases {
243            let usage = TokenUsage {
244                input_tokens: input,
245                output_tokens: output,
246            };
247            assert_eq!(
248                usage.total(),
249                expected,
250                "Failed for input={}, output={}",
251                input,
252                output
253            );
254        }
255    }
256}