1use std::time::{Duration, Instant};
10
11use serde::{Deserialize, Serialize};
12
13use bamboo_agent_core::AgentEvent;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ToolEventPhase {
19 Begin,
20 Executing,
21 Finished,
22 Error,
23 Cancelled,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ToolEvent {
29 pub call_id: String,
31 pub tool_name: String,
33 pub phase: ToolEventPhase,
35 pub elapsed_ms: Option<u64>,
37 pub is_mutating: bool,
39 pub auto_approved: bool,
41 pub summary: Option<String>,
43 pub error: Option<String>,
45}
46
47impl ToolEvent {
48 pub fn into_agent_event(self) -> AgentEvent {
51 let phase_str = match self.phase {
52 ToolEventPhase::Begin => "begin",
53 ToolEventPhase::Executing => "executing",
54 ToolEventPhase::Finished => "finished",
55 ToolEventPhase::Error => "error",
56 ToolEventPhase::Cancelled => "cancelled",
57 };
58 AgentEvent::ToolLifecycle {
59 tool_call_id: self.call_id,
60 tool_name: self.tool_name,
61 phase: phase_str.to_string(),
62 elapsed_ms: self.elapsed_ms,
63 is_mutating: self.is_mutating,
64 auto_approved: self.auto_approved,
65 summary: self.summary,
66 error: self.error,
67 }
68 }
69}
70
71#[derive(Debug)]
81pub struct ToolEmitter {
82 call_id: String,
83 tool_name: String,
84 is_mutating: bool,
85 auto_approved: bool,
86 started_at: Instant,
87 events: Vec<ToolEvent>,
88}
89
90impl ToolEmitter {
91 pub fn new(call_id: &str, tool_name: &str, is_mutating: bool) -> Self {
93 Self {
94 call_id: call_id.to_string(),
95 tool_name: tool_name.to_string(),
96 is_mutating,
97 auto_approved: false,
98 started_at: Instant::now(),
99 events: Vec::new(),
100 }
101 }
102
103 pub fn set_auto_approved(&mut self, auto_approved: bool) {
105 self.auto_approved = auto_approved;
106 }
107
108 pub fn begin(&mut self) -> &ToolEvent {
110 let event = ToolEvent {
111 call_id: self.call_id.clone(),
112 tool_name: self.tool_name.clone(),
113 phase: ToolEventPhase::Begin,
114 elapsed_ms: None,
115 is_mutating: self.is_mutating,
116 auto_approved: self.auto_approved,
117 summary: None,
118 error: None,
119 };
120 self.events.push(event);
121 self.events.last().unwrap()
122 }
123
124 pub fn finish(&mut self, summary: Option<String>) -> &ToolEvent {
126 let elapsed = self.started_at.elapsed();
127 let event = ToolEvent {
128 call_id: self.call_id.clone(),
129 tool_name: self.tool_name.clone(),
130 phase: ToolEventPhase::Finished,
131 elapsed_ms: Some(elapsed.as_millis() as u64),
132 is_mutating: self.is_mutating,
133 auto_approved: self.auto_approved,
134 summary,
135 error: None,
136 };
137 self.events.push(event);
138 self.events.last().unwrap()
139 }
140
141 pub fn error(&mut self, error: String) -> &ToolEvent {
143 let elapsed = self.started_at.elapsed();
144 let event = ToolEvent {
145 call_id: self.call_id.clone(),
146 tool_name: self.tool_name.clone(),
147 phase: ToolEventPhase::Error,
148 elapsed_ms: Some(elapsed.as_millis() as u64),
149 is_mutating: self.is_mutating,
150 auto_approved: self.auto_approved,
151 summary: None,
152 error: Some(error),
153 };
154 self.events.push(event);
155 self.events.last().unwrap()
156 }
157
158 pub fn cancelled(&mut self, reason: Option<String>) -> &ToolEvent {
160 let elapsed = self.started_at.elapsed();
161 let event = ToolEvent {
162 call_id: self.call_id.clone(),
163 tool_name: self.tool_name.clone(),
164 phase: ToolEventPhase::Cancelled,
165 elapsed_ms: Some(elapsed.as_millis() as u64),
166 is_mutating: self.is_mutating,
167 auto_approved: self.auto_approved,
168 summary: reason,
169 error: None,
170 };
171 self.events.push(event);
172 self.events.last().unwrap()
173 }
174
175 pub fn elapsed(&self) -> Duration {
177 self.started_at.elapsed()
178 }
179
180 pub fn events(&self) -> &[ToolEvent] {
182 &self.events
183 }
184
185 pub fn call_id(&self) -> &str {
187 &self.call_id
188 }
189
190 pub fn tool_name(&self) -> &str {
192 &self.tool_name
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_emitter_lifecycle() {
202 let mut emitter = ToolEmitter::new("call_1", "Edit", true);
203 emitter.set_auto_approved(true);
204
205 let begin = emitter.begin();
206 assert_eq!(begin.phase, ToolEventPhase::Begin);
207 assert!(begin.is_mutating);
208 assert!(begin.auto_approved);
209 assert!(begin.elapsed_ms.is_none());
210
211 let finish = emitter.finish(Some("Updated 2 files".to_string()));
212 assert_eq!(finish.phase, ToolEventPhase::Finished);
213 assert!(finish.elapsed_ms.is_some());
214 assert_eq!(finish.summary.as_deref(), Some("Updated 2 files"));
215
216 assert_eq!(emitter.events().len(), 2);
217 }
218
219 #[test]
220 fn test_emitter_error() {
221 let mut emitter = ToolEmitter::new("call_2", "Bash", true);
222 emitter.begin();
223 let err = emitter.error("Permission denied".to_string());
224 assert_eq!(err.phase, ToolEventPhase::Error);
225 assert_eq!(err.error.as_deref(), Some("Permission denied"));
226 assert_eq!(emitter.events().len(), 2);
227 }
228
229 #[test]
230 fn test_emitter_cancelled() {
231 let mut emitter = ToolEmitter::new("call_3", "Write", true);
232 emitter.begin();
233 let cancel = emitter.cancelled(Some("User denied".to_string()));
234 assert_eq!(cancel.phase, ToolEventPhase::Cancelled);
235 assert_eq!(cancel.summary.as_deref(), Some("User denied"));
236 }
237
238 #[test]
239 fn test_non_mutating_tool() {
240 let mut emitter = ToolEmitter::new("call_4", "Read", false);
241 let begin = emitter.begin();
242 assert!(!begin.is_mutating);
243 assert!(!begin.auto_approved);
244 }
245
246 #[test]
247 fn test_serialization() {
248 let event = ToolEvent {
249 call_id: "c1".to_string(),
250 tool_name: "Bash".to_string(),
251 phase: ToolEventPhase::Finished,
252 elapsed_ms: Some(150),
253 is_mutating: true,
254 auto_approved: false,
255 summary: Some("Ran command".to_string()),
256 error: None,
257 };
258 let json = serde_json::to_string(&event).unwrap();
259 assert!(json.contains("\"phase\":\"finished\""));
260 assert!(json.contains("\"elapsed_ms\":150"));
261 let deserialized: ToolEvent = serde_json::from_str(&json).unwrap();
262 assert_eq!(deserialized.phase, ToolEventPhase::Finished);
263 }
264
265 #[test]
266 fn test_into_agent_event_begin() {
267 let mut emitter = ToolEmitter::new("call_99", "Read", false);
268 let event = emitter.begin().clone();
269 let agent_event = event.into_agent_event();
270
271 match agent_event {
272 AgentEvent::ToolLifecycle {
273 tool_call_id,
274 tool_name,
275 phase,
276 elapsed_ms,
277 is_mutating,
278 auto_approved,
279 ..
280 } => {
281 assert_eq!(tool_call_id, "call_99");
282 assert_eq!(tool_name, "Read");
283 assert_eq!(phase, "begin");
284 assert!(elapsed_ms.is_none());
285 assert!(!is_mutating);
286 assert!(!auto_approved);
287 }
288 _ => panic!("Expected ToolLifecycle variant"),
289 }
290 }
291
292 #[test]
293 fn test_into_agent_event_finished() {
294 let mut emitter = ToolEmitter::new("call_100", "Bash", true);
295 emitter.set_auto_approved(false);
296 emitter.begin();
297 std::thread::sleep(std::time::Duration::from_millis(5));
298 let event = emitter.finish(Some("done".to_string())).clone();
299 let agent_event = event.into_agent_event();
300
301 match agent_event {
302 AgentEvent::ToolLifecycle {
303 phase,
304 elapsed_ms,
305 is_mutating,
306 summary,
307 ..
308 } => {
309 assert_eq!(phase, "finished");
310 assert!(elapsed_ms.unwrap() >= 5);
311 assert!(is_mutating);
312 assert_eq!(summary.as_deref(), Some("done"));
313 }
314 _ => panic!("Expected ToolLifecycle variant"),
315 }
316 }
317
318 #[test]
319 fn test_into_agent_event_error() {
320 let mut emitter = ToolEmitter::new("call_101", "Write", true);
321 emitter.begin();
322 let event = emitter.error("Permission denied".to_string()).clone();
323 let agent_event = event.into_agent_event();
324
325 match agent_event {
326 AgentEvent::ToolLifecycle { phase, error, .. } => {
327 assert_eq!(phase, "error");
328 assert_eq!(error.as_deref(), Some("Permission denied"));
329 }
330 _ => panic!("Expected ToolLifecycle variant"),
331 }
332 }
333
334 #[test]
335 fn test_into_agent_event_cancelled() {
336 let mut emitter = ToolEmitter::new("call_102", "Edit", true);
337 emitter.begin();
338 let event = emitter.cancelled(Some("User denied".to_string())).clone();
339 let agent_event = event.into_agent_event();
340
341 match agent_event {
342 AgentEvent::ToolLifecycle { phase, summary, .. } => {
343 assert_eq!(phase, "cancelled");
344 assert_eq!(summary.as_deref(), Some("User denied"));
345 }
346 _ => panic!("Expected ToolLifecycle variant"),
347 }
348 }
349
350 #[test]
351 fn test_agent_event_serialization_roundtrip() {
352 let event = ToolEvent {
353 call_id: "c1".to_string(),
354 tool_name: "Bash".to_string(),
355 phase: ToolEventPhase::Finished,
356 elapsed_ms: Some(42),
357 is_mutating: true,
358 auto_approved: false,
359 summary: Some("ok".to_string()),
360 error: None,
361 };
362 let agent_event = event.into_agent_event();
363 let json = serde_json::to_string(&agent_event).unwrap();
364 assert!(json.contains("\"type\":\"tool_lifecycle\""));
365 assert!(json.contains("\"phase\":\"finished\""));
366 assert!(json.contains("\"elapsed_ms\":42"));
367 }
368}