Skip to main content

agent_event_log/
lib.rs

1/*!
2agent-event-log: structured lifecycle event log for AI agent runs.
3
4Records agent run events (started, tool_called, response, error, finished)
5in an append-only log with monotonic sequence numbers and optional metadata.
6
7```rust
8use agent_event_log::{EventLog, EventKind};
9
10let mut log = EventLog::new("run-1");
11log.started();
12log.tool_called("search", serde_json::json!({"q": "rust"}));
13log.finished("done");
14assert_eq!(log.len(), 3);
15```
16*/
17
18use serde_json::Value;
19
20#[derive(Debug, Clone, PartialEq)]
21pub enum EventKind {
22    Started,
23    ToolCalled,
24    ToolResult,
25    Response,
26    Error,
27    Finished,
28    Custom(String),
29}
30
31impl EventKind {
32    pub fn label(&self) -> &str {
33        match self {
34            EventKind::Started => "STARTED",
35            EventKind::ToolCalled => "TOOL_CALLED",
36            EventKind::ToolResult => "TOOL_RESULT",
37            EventKind::Response => "RESPONSE",
38            EventKind::Error => "ERROR",
39            EventKind::Finished => "FINISHED",
40            EventKind::Custom(s) => s.as_str(),
41        }
42    }
43
44    pub fn is_terminal(&self) -> bool {
45        matches!(self, EventKind::Finished | EventKind::Error)
46    }
47}
48
49#[derive(Debug, Clone)]
50pub struct Event {
51    pub seq: usize,
52    pub run_id: String,
53    pub kind: EventKind,
54    pub message: String,
55    pub metadata: Option<Value>,
56}
57
58/// Append-only event log for one agent run.
59#[derive(Debug)]
60pub struct EventLog {
61    pub run_id: String,
62    events: Vec<Event>,
63}
64
65impl EventLog {
66    pub fn new(run_id: impl Into<String>) -> Self {
67        Self { run_id: run_id.into(), events: Vec::new() }
68    }
69
70    fn push(&mut self, kind: EventKind, message: impl Into<String>, meta: Option<Value>) {
71        let seq = self.events.len();
72        self.events.push(Event {
73            seq,
74            run_id: self.run_id.clone(),
75            kind,
76            message: message.into(),
77            metadata: meta,
78        });
79    }
80
81    pub fn started(&mut self) { self.push(EventKind::Started, "agent started", None); }
82
83    pub fn tool_called(&mut self, tool: impl Into<String>, args: Value) {
84        let tool = tool.into();
85        let msg = format!("calling {}", tool);
86        self.push(EventKind::ToolCalled, msg, Some(args));
87    }
88
89    pub fn tool_result(&mut self, tool: impl Into<String>, result: Value) {
90        let msg = format!("result from {}", tool.into());
91        self.push(EventKind::ToolResult, msg, Some(result));
92    }
93
94    pub fn response(&mut self, text: impl Into<String>) {
95        self.push(EventKind::Response, text, None);
96    }
97
98    pub fn error(&mut self, err: impl Into<String>) {
99        self.push(EventKind::Error, err, None);
100    }
101
102    pub fn finished(&mut self, reason: impl Into<String>) {
103        self.push(EventKind::Finished, reason, None);
104    }
105
106    pub fn custom(&mut self, label: impl Into<String>, msg: impl Into<String>) {
107        self.push(EventKind::Custom(label.into()), msg, None);
108    }
109
110    pub fn len(&self) -> usize { self.events.len() }
111    pub fn is_empty(&self) -> bool { self.events.is_empty() }
112    pub fn events(&self) -> &[Event] { &self.events }
113
114    pub fn by_kind(&self, kind: &EventKind) -> Vec<&Event> {
115        self.events.iter().filter(|e| &e.kind == kind).collect()
116    }
117
118    pub fn has_error(&self) -> bool {
119        self.events.iter().any(|e| e.kind == EventKind::Error)
120    }
121
122    pub fn is_finished(&self) -> bool {
123        self.events.iter().any(|e| e.kind.is_terminal())
124    }
125
126    pub fn last(&self) -> Option<&Event> { self.events.last() }
127
128    pub fn tool_calls(&self) -> Vec<&Event> {
129        self.by_kind(&EventKind::ToolCalled)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use serde_json::json;
137
138    #[test]
139    fn empty_log() {
140        let log = EventLog::new("r1");
141        assert!(log.is_empty());
142        assert_eq!(log.run_id, "r1");
143    }
144
145    #[test]
146    fn started_adds_event() {
147        let mut log = EventLog::new("r1");
148        log.started();
149        assert_eq!(log.len(), 1);
150        assert_eq!(log.events()[0].kind, EventKind::Started);
151    }
152
153    #[test]
154    fn tool_called_has_metadata() {
155        let mut log = EventLog::new("r1");
156        log.tool_called("search", json!({"q": "rust"}));
157        let e = &log.events()[0];
158        assert!(e.metadata.is_some());
159    }
160
161    #[test]
162    fn tool_result_recorded() {
163        let mut log = EventLog::new("r1");
164        log.tool_result("search", json!(["result1"]));
165        assert_eq!(log.by_kind(&EventKind::ToolResult).len(), 1);
166    }
167
168    #[test]
169    fn response_event() {
170        let mut log = EventLog::new("r1");
171        log.response("Hello!");
172        assert_eq!(log.events()[0].message, "Hello!");
173    }
174
175    #[test]
176    fn error_detected() {
177        let mut log = EventLog::new("r1");
178        log.error("timeout");
179        assert!(log.has_error());
180    }
181
182    #[test]
183    fn finished_is_terminal() {
184        let mut log = EventLog::new("r1");
185        log.finished("success");
186        assert!(log.is_finished());
187    }
188
189    #[test]
190    fn seq_increments() {
191        let mut log = EventLog::new("r1");
192        log.started();
193        log.response("ok");
194        log.finished("done");
195        let seqs: Vec<usize> = log.events().iter().map(|e| e.seq).collect();
196        assert_eq!(seqs, vec![0, 1, 2]);
197    }
198
199    #[test]
200    fn by_kind_filter() {
201        let mut log = EventLog::new("r1");
202        log.tool_called("a", json!({}));
203        log.tool_called("b", json!({}));
204        log.response("ok");
205        assert_eq!(log.tool_calls().len(), 2);
206    }
207
208    #[test]
209    fn last_event() {
210        let mut log = EventLog::new("r1");
211        log.started();
212        log.finished("done");
213        assert_eq!(log.last().unwrap().kind, EventKind::Finished);
214    }
215
216    #[test]
217    fn custom_event() {
218        let mut log = EventLog::new("r1");
219        log.custom("THINKING", "deciding what to do");
220        assert_eq!(log.events()[0].kind, EventKind::Custom("THINKING".into()));
221    }
222
223    #[test]
224    fn no_error_by_default() {
225        let log = EventLog::new("r1");
226        assert!(!log.has_error());
227    }
228
229    #[test]
230    fn not_finished_without_terminal_event() {
231        let mut log = EventLog::new("r1");
232        log.started();
233        log.response("hello");
234        assert!(!log.is_finished());
235    }
236}