use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum EventKind {
Started,
ToolCalled,
ToolResult,
Response,
Error,
Finished,
Custom(String),
}
impl EventKind {
pub fn label(&self) -> &str {
match self {
EventKind::Started => "STARTED",
EventKind::ToolCalled => "TOOL_CALLED",
EventKind::ToolResult => "TOOL_RESULT",
EventKind::Response => "RESPONSE",
EventKind::Error => "ERROR",
EventKind::Finished => "FINISHED",
EventKind::Custom(s) => s.as_str(),
}
}
pub fn is_terminal(&self) -> bool {
matches!(self, EventKind::Finished | EventKind::Error)
}
}
#[derive(Debug, Clone)]
pub struct Event {
pub seq: usize,
pub run_id: String,
pub kind: EventKind,
pub message: String,
pub metadata: Option<Value>,
}
#[derive(Debug)]
pub struct EventLog {
pub run_id: String,
events: Vec<Event>,
}
impl EventLog {
pub fn new(run_id: impl Into<String>) -> Self {
Self { run_id: run_id.into(), events: Vec::new() }
}
fn push(&mut self, kind: EventKind, message: impl Into<String>, meta: Option<Value>) {
let seq = self.events.len();
self.events.push(Event {
seq,
run_id: self.run_id.clone(),
kind,
message: message.into(),
metadata: meta,
});
}
pub fn started(&mut self) { self.push(EventKind::Started, "agent started", None); }
pub fn tool_called(&mut self, tool: impl Into<String>, args: Value) {
let tool = tool.into();
let msg = format!("calling {}", tool);
self.push(EventKind::ToolCalled, msg, Some(args));
}
pub fn tool_result(&mut self, tool: impl Into<String>, result: Value) {
let msg = format!("result from {}", tool.into());
self.push(EventKind::ToolResult, msg, Some(result));
}
pub fn response(&mut self, text: impl Into<String>) {
self.push(EventKind::Response, text, None);
}
pub fn error(&mut self, err: impl Into<String>) {
self.push(EventKind::Error, err, None);
}
pub fn finished(&mut self, reason: impl Into<String>) {
self.push(EventKind::Finished, reason, None);
}
pub fn custom(&mut self, label: impl Into<String>, msg: impl Into<String>) {
self.push(EventKind::Custom(label.into()), msg, None);
}
pub fn len(&self) -> usize { self.events.len() }
pub fn is_empty(&self) -> bool { self.events.is_empty() }
pub fn events(&self) -> &[Event] { &self.events }
pub fn by_kind(&self, kind: &EventKind) -> Vec<&Event> {
self.events.iter().filter(|e| &e.kind == kind).collect()
}
pub fn has_error(&self) -> bool {
self.events.iter().any(|e| e.kind == EventKind::Error)
}
pub fn is_finished(&self) -> bool {
self.events.iter().any(|e| e.kind.is_terminal())
}
pub fn last(&self) -> Option<&Event> { self.events.last() }
pub fn tool_calls(&self) -> Vec<&Event> {
self.by_kind(&EventKind::ToolCalled)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn empty_log() {
let log = EventLog::new("r1");
assert!(log.is_empty());
assert_eq!(log.run_id, "r1");
}
#[test]
fn started_adds_event() {
let mut log = EventLog::new("r1");
log.started();
assert_eq!(log.len(), 1);
assert_eq!(log.events()[0].kind, EventKind::Started);
}
#[test]
fn tool_called_has_metadata() {
let mut log = EventLog::new("r1");
log.tool_called("search", json!({"q": "rust"}));
let e = &log.events()[0];
assert!(e.metadata.is_some());
}
#[test]
fn tool_result_recorded() {
let mut log = EventLog::new("r1");
log.tool_result("search", json!(["result1"]));
assert_eq!(log.by_kind(&EventKind::ToolResult).len(), 1);
}
#[test]
fn response_event() {
let mut log = EventLog::new("r1");
log.response("Hello!");
assert_eq!(log.events()[0].message, "Hello!");
}
#[test]
fn error_detected() {
let mut log = EventLog::new("r1");
log.error("timeout");
assert!(log.has_error());
}
#[test]
fn finished_is_terminal() {
let mut log = EventLog::new("r1");
log.finished("success");
assert!(log.is_finished());
}
#[test]
fn seq_increments() {
let mut log = EventLog::new("r1");
log.started();
log.response("ok");
log.finished("done");
let seqs: Vec<usize> = log.events().iter().map(|e| e.seq).collect();
assert_eq!(seqs, vec![0, 1, 2]);
}
#[test]
fn by_kind_filter() {
let mut log = EventLog::new("r1");
log.tool_called("a", json!({}));
log.tool_called("b", json!({}));
log.response("ok");
assert_eq!(log.tool_calls().len(), 2);
}
#[test]
fn last_event() {
let mut log = EventLog::new("r1");
log.started();
log.finished("done");
assert_eq!(log.last().unwrap().kind, EventKind::Finished);
}
#[test]
fn custom_event() {
let mut log = EventLog::new("r1");
log.custom("THINKING", "deciding what to do");
assert_eq!(log.events()[0].kind, EventKind::Custom("THINKING".into()));
}
#[test]
fn no_error_by_default() {
let log = EventLog::new("r1");
assert!(!log.has_error());
}
#[test]
fn not_finished_without_terminal_event() {
let mut log = EventLog::new("r1");
log.started();
log.response("hello");
assert!(!log.is_finished());
}
}