use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TurnStartEvent {
pub prompt: String,
pub timestamp_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkingDeltaEvent {
pub content: String,
pub is_first: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolApprovalEvent {
pub call_id: String,
pub tool_name: String,
pub arguments: serde_json::Value,
pub auto_approved: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolStartEvent {
pub call_id: String,
pub tool_name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCompleteEvent {
pub call_id: String,
pub tool_name: String,
pub result: String,
pub success: bool,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TurnEndEvent {
pub content: String,
pub tool_call_count: usize,
pub iterations: usize,
pub usage: TokenUsageInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TokenUsageInfo {
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub total_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionEndEvent {
pub content: String,
pub total_iterations: usize,
pub usage: TokenUsageInfo,
pub finish_reason: FinishReason,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "message")]
pub enum FinishReason {
Stop,
MaxIterations,
Error(String),
UnknownTool(String),
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event_type")]
pub enum AgentEvent {
TurnStart(TurnStartEvent),
ThinkingDelta(ThinkingDeltaEvent),
ToolApproval(ToolApprovalEvent),
ToolStart(ToolStartEvent),
ToolComplete(ToolCompleteEvent),
TurnEnd(TurnEndEvent),
SessionEnd(SessionEndEvent),
}
impl AgentEvent {
pub fn turn_start(prompt: &str) -> Self {
AgentEvent::TurnStart(TurnStartEvent {
prompt: prompt.to_string(),
timestamp_secs: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
})
}
pub fn thinking_delta(content: &str, is_first: bool) -> Self {
AgentEvent::ThinkingDelta(ThinkingDeltaEvent {
content: content.to_string(),
is_first,
})
}
pub fn tool_approval(call_id: &str, tool_name: &str, arguments: serde_json::Value, auto_approved: bool) -> Self {
AgentEvent::ToolApproval(ToolApprovalEvent {
call_id: call_id.to_string(),
tool_name: tool_name.to_string(),
arguments,
auto_approved,
})
}
pub fn tool_start(call_id: &str, tool_name: &str, arguments: serde_json::Value) -> Self {
AgentEvent::ToolStart(ToolStartEvent {
call_id: call_id.to_string(),
tool_name: tool_name.to_string(),
arguments,
})
}
pub fn tool_complete(call_id: &str, tool_name: &str, result: &str, success: bool, duration_ms: u64) -> Self {
AgentEvent::ToolComplete(ToolCompleteEvent {
call_id: call_id.to_string(),
tool_name: tool_name.to_string(),
result: result.to_string(),
success,
duration_ms,
})
}
pub fn turn_end(content: &str, tool_call_count: usize, iterations: usize, usage: TokenUsageInfo) -> Self {
AgentEvent::TurnEnd(TurnEndEvent {
content: content.to_string(),
tool_call_count,
iterations,
usage,
})
}
pub fn session_end(content: &str, total_iterations: usize, usage: TokenUsageInfo, reason: FinishReason) -> Self {
AgentEvent::SessionEnd(SessionEndEvent {
content: content.to_string(),
total_iterations,
usage,
finish_reason: reason,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_turn_start_event() {
let event = AgentEvent::turn_start("Hello, world!");
assert!(matches!(event, AgentEvent::TurnStart(_)));
}
#[test]
fn test_thinking_delta_event() {
let event = AgentEvent::thinking_delta("Hello", true);
assert!(matches!(event, AgentEvent::ThinkingDelta(_)));
}
#[test]
fn test_tool_approval_event() {
let args = serde_json::json!({"command": "ls"});
let event = AgentEvent::tool_approval("call_1", "bash", args, true);
assert!(matches!(event, AgentEvent::ToolApproval(_)));
}
#[test]
fn test_tool_start_event() {
let args = serde_json::json!({"command": "ls"});
let event = AgentEvent::tool_start("call_1", "bash", args);
assert!(matches!(event, AgentEvent::ToolStart(_)));
}
#[test]
fn test_tool_complete_event() {
let event = AgentEvent::tool_complete("call_1", "bash", "file1\nfile2", true, 100);
assert!(matches!(event, AgentEvent::ToolComplete(_)));
}
#[test]
fn test_turn_end_event() {
let usage = TokenUsageInfo {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let event = AgentEvent::turn_end("Done!", 2, 5, usage);
assert!(matches!(event, AgentEvent::TurnEnd(_)));
}
#[test]
fn test_session_end_event() {
let usage = TokenUsageInfo::default();
let event = AgentEvent::session_end("Goodbye!", 10, usage, FinishReason::Stop);
assert!(matches!(event, AgentEvent::SessionEnd(_)));
}
#[test]
fn test_finish_reason_variants() {
let stop = FinishReason::Stop;
let max_iter = FinishReason::MaxIterations;
let error = FinishReason::Error("something went wrong".to_string());
let unknown = FinishReason::UnknownTool("fake_tool".to_string());
let cancelled = FinishReason::Cancelled;
assert!(matches!(stop, FinishReason::Stop));
assert!(matches!(max_iter, FinishReason::MaxIterations));
assert!(matches!(error, FinishReason::Error(_)));
assert!(matches!(unknown, FinishReason::UnknownTool(_)));
assert!(matches!(cancelled, FinishReason::Cancelled));
}
#[test]
fn test_event_serialization() {
let event = AgentEvent::turn_start("test prompt");
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("TurnStart"));
assert!(json.contains("test prompt"));
}
}