agent-event-log 0.1.0

Structured lifecycle event log for AI agent runs
Documentation
/*!
agent-event-log: structured lifecycle event log for AI agent runs.

Records agent run events (started, tool_called, response, error, finished)
in an append-only log with monotonic sequence numbers and optional metadata.

```rust
use agent_event_log::{EventLog, EventKind};

let mut log = EventLog::new("run-1");
log.started();
log.tool_called("search", serde_json::json!({"q": "rust"}));
log.finished("done");
assert_eq!(log.len(), 3);
```
*/

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>,
}

/// Append-only event log for one agent run.
#[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());
    }
}