use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use bamboo_agent_core::AgentEvent;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolEventPhase {
Begin,
Executing,
Finished,
Error,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolEvent {
pub call_id: String,
pub tool_name: String,
pub phase: ToolEventPhase,
pub elapsed_ms: Option<u64>,
pub is_mutating: bool,
pub auto_approved: bool,
pub summary: Option<String>,
pub error: Option<String>,
}
impl ToolEvent {
pub fn into_agent_event(self) -> AgentEvent {
let phase_str = match self.phase {
ToolEventPhase::Begin => "begin",
ToolEventPhase::Executing => "executing",
ToolEventPhase::Finished => "finished",
ToolEventPhase::Error => "error",
ToolEventPhase::Cancelled => "cancelled",
};
AgentEvent::ToolLifecycle {
tool_call_id: self.call_id,
tool_name: self.tool_name,
phase: phase_str.to_string(),
elapsed_ms: self.elapsed_ms,
is_mutating: self.is_mutating,
auto_approved: self.auto_approved,
summary: self.summary,
error: self.error,
}
}
}
#[derive(Debug)]
pub struct ToolEmitter {
call_id: String,
tool_name: String,
is_mutating: bool,
auto_approved: bool,
started_at: Instant,
events: Vec<ToolEvent>,
}
impl ToolEmitter {
pub fn new(call_id: &str, tool_name: &str, is_mutating: bool) -> Self {
Self {
call_id: call_id.to_string(),
tool_name: tool_name.to_string(),
is_mutating,
auto_approved: false,
started_at: Instant::now(),
events: Vec::new(),
}
}
pub fn set_auto_approved(&mut self, auto_approved: bool) {
self.auto_approved = auto_approved;
}
pub fn begin(&mut self) -> &ToolEvent {
let event = ToolEvent {
call_id: self.call_id.clone(),
tool_name: self.tool_name.clone(),
phase: ToolEventPhase::Begin,
elapsed_ms: None,
is_mutating: self.is_mutating,
auto_approved: self.auto_approved,
summary: None,
error: None,
};
self.events.push(event);
self.events.last().unwrap()
}
pub fn finish(&mut self, summary: Option<String>) -> &ToolEvent {
let elapsed = self.started_at.elapsed();
let event = ToolEvent {
call_id: self.call_id.clone(),
tool_name: self.tool_name.clone(),
phase: ToolEventPhase::Finished,
elapsed_ms: Some(elapsed.as_millis() as u64),
is_mutating: self.is_mutating,
auto_approved: self.auto_approved,
summary,
error: None,
};
self.events.push(event);
self.events.last().unwrap()
}
pub fn error(&mut self, error: String) -> &ToolEvent {
let elapsed = self.started_at.elapsed();
let event = ToolEvent {
call_id: self.call_id.clone(),
tool_name: self.tool_name.clone(),
phase: ToolEventPhase::Error,
elapsed_ms: Some(elapsed.as_millis() as u64),
is_mutating: self.is_mutating,
auto_approved: self.auto_approved,
summary: None,
error: Some(error),
};
self.events.push(event);
self.events.last().unwrap()
}
pub fn cancelled(&mut self, reason: Option<String>) -> &ToolEvent {
let elapsed = self.started_at.elapsed();
let event = ToolEvent {
call_id: self.call_id.clone(),
tool_name: self.tool_name.clone(),
phase: ToolEventPhase::Cancelled,
elapsed_ms: Some(elapsed.as_millis() as u64),
is_mutating: self.is_mutating,
auto_approved: self.auto_approved,
summary: reason,
error: None,
};
self.events.push(event);
self.events.last().unwrap()
}
pub fn elapsed(&self) -> Duration {
self.started_at.elapsed()
}
pub fn events(&self) -> &[ToolEvent] {
&self.events
}
pub fn call_id(&self) -> &str {
&self.call_id
}
pub fn tool_name(&self) -> &str {
&self.tool_name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emitter_lifecycle() {
let mut emitter = ToolEmitter::new("call_1", "Edit", true);
emitter.set_auto_approved(true);
let begin = emitter.begin();
assert_eq!(begin.phase, ToolEventPhase::Begin);
assert!(begin.is_mutating);
assert!(begin.auto_approved);
assert!(begin.elapsed_ms.is_none());
let finish = emitter.finish(Some("Updated 2 files".to_string()));
assert_eq!(finish.phase, ToolEventPhase::Finished);
assert!(finish.elapsed_ms.is_some());
assert_eq!(finish.summary.as_deref(), Some("Updated 2 files"));
assert_eq!(emitter.events().len(), 2);
}
#[test]
fn test_emitter_error() {
let mut emitter = ToolEmitter::new("call_2", "Bash", true);
emitter.begin();
let err = emitter.error("Permission denied".to_string());
assert_eq!(err.phase, ToolEventPhase::Error);
assert_eq!(err.error.as_deref(), Some("Permission denied"));
assert_eq!(emitter.events().len(), 2);
}
#[test]
fn test_emitter_cancelled() {
let mut emitter = ToolEmitter::new("call_3", "Write", true);
emitter.begin();
let cancel = emitter.cancelled(Some("User denied".to_string()));
assert_eq!(cancel.phase, ToolEventPhase::Cancelled);
assert_eq!(cancel.summary.as_deref(), Some("User denied"));
}
#[test]
fn test_non_mutating_tool() {
let mut emitter = ToolEmitter::new("call_4", "Read", false);
let begin = emitter.begin();
assert!(!begin.is_mutating);
assert!(!begin.auto_approved);
}
#[test]
fn test_serialization() {
let event = ToolEvent {
call_id: "c1".to_string(),
tool_name: "Bash".to_string(),
phase: ToolEventPhase::Finished,
elapsed_ms: Some(150),
is_mutating: true,
auto_approved: false,
summary: Some("Ran command".to_string()),
error: None,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"phase\":\"finished\""));
assert!(json.contains("\"elapsed_ms\":150"));
let deserialized: ToolEvent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.phase, ToolEventPhase::Finished);
}
#[test]
fn test_into_agent_event_begin() {
let mut emitter = ToolEmitter::new("call_99", "Read", false);
let event = emitter.begin().clone();
let agent_event = event.into_agent_event();
match agent_event {
AgentEvent::ToolLifecycle {
tool_call_id,
tool_name,
phase,
elapsed_ms,
is_mutating,
auto_approved,
..
} => {
assert_eq!(tool_call_id, "call_99");
assert_eq!(tool_name, "Read");
assert_eq!(phase, "begin");
assert!(elapsed_ms.is_none());
assert!(!is_mutating);
assert!(!auto_approved);
}
_ => panic!("Expected ToolLifecycle variant"),
}
}
#[test]
fn test_into_agent_event_finished() {
let mut emitter = ToolEmitter::new("call_100", "Bash", true);
emitter.set_auto_approved(false);
emitter.begin();
std::thread::sleep(std::time::Duration::from_millis(5));
let event = emitter.finish(Some("done".to_string())).clone();
let agent_event = event.into_agent_event();
match agent_event {
AgentEvent::ToolLifecycle {
phase,
elapsed_ms,
is_mutating,
summary,
..
} => {
assert_eq!(phase, "finished");
assert!(elapsed_ms.unwrap() >= 5);
assert!(is_mutating);
assert_eq!(summary.as_deref(), Some("done"));
}
_ => panic!("Expected ToolLifecycle variant"),
}
}
#[test]
fn test_into_agent_event_error() {
let mut emitter = ToolEmitter::new("call_101", "Write", true);
emitter.begin();
let event = emitter.error("Permission denied".to_string()).clone();
let agent_event = event.into_agent_event();
match agent_event {
AgentEvent::ToolLifecycle { phase, error, .. } => {
assert_eq!(phase, "error");
assert_eq!(error.as_deref(), Some("Permission denied"));
}
_ => panic!("Expected ToolLifecycle variant"),
}
}
#[test]
fn test_into_agent_event_cancelled() {
let mut emitter = ToolEmitter::new("call_102", "Edit", true);
emitter.begin();
let event = emitter.cancelled(Some("User denied".to_string())).clone();
let agent_event = event.into_agent_event();
match agent_event {
AgentEvent::ToolLifecycle { phase, summary, .. } => {
assert_eq!(phase, "cancelled");
assert_eq!(summary.as_deref(), Some("User denied"));
}
_ => panic!("Expected ToolLifecycle variant"),
}
}
#[test]
fn test_agent_event_serialization_roundtrip() {
let event = ToolEvent {
call_id: "c1".to_string(),
tool_name: "Bash".to_string(),
phase: ToolEventPhase::Finished,
elapsed_ms: Some(42),
is_mutating: true,
auto_approved: false,
summary: Some("ok".to_string()),
error: None,
};
let agent_event = event.into_agent_event();
let json = serde_json::to_string(&agent_event).unwrap();
assert!(json.contains("\"type\":\"tool_lifecycle\""));
assert!(json.contains("\"phase\":\"finished\""));
assert!(json.contains("\"elapsed_ms\":42"));
}
}