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