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}