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}