1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "UPPERCASE")]
10pub enum RunStatus {
11 Running,
12 Completed,
13 Failed,
14 Cancelled,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct Run {
20 pub run_id: Uuid,
22
23 pub agent_spec_digest: String,
25
26 pub git_sha: String,
28
29 pub started_at: DateTime<Utc>,
31
32 pub finished_at: Option<DateTime<Utc>>,
34
35 pub status: RunStatus,
37
38 pub inputs: serde_json::Value,
40
41 pub outputs: Option<serde_json::Value>,
43
44 pub final_state_digest: Option<String>,
46}
47
48impl Run {
49 pub fn new(agent_spec_digest: String, git_sha: String, inputs: serde_json::Value) -> Self {
51 Self {
52 run_id: Uuid::new_v4(),
53 agent_spec_digest,
54 git_sha,
55 started_at: Utc::now(),
56 finished_at: None,
57 status: RunStatus::Running,
58 inputs,
59 outputs: None,
60 final_state_digest: None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum EventKind {
69 GraphStarted,
71
72 GraphCompleted { iterations: u32, duration_ms: u64 },
74
75 GraphFailed { error: String },
77
78 NodeEntered { node_id: String, iteration: u32 },
80
81 NodeExited {
83 node_id: String,
84 next_node: Option<String>,
85 duration_ms: u64,
86 },
87
88 NodeFailed { node_id: String, error: String },
90
91 ToolCalled { tool_name: String },
93
94 ToolReturned { tool_name: String },
96
97 ToolFailed { tool_name: String },
99
100 CheckpointSaved {
102 checkpoint_id: String,
103 node_id: String,
104 },
105
106 CheckpointRestored {
108 checkpoint_id: String,
109 node_id: String,
110 },
111
112 CheckpointDeleted { checkpoint_id: String },
114
115 StateUpdated {
117 node_id: String,
118 keys_changed: Vec<String>,
119 },
120
121 MessageAdded { role: String, content_length: usize },
123
124 GraphInterrupted { reason: String, node_id: String },
126
127 NodeRetrying {
129 node_id: String,
130 attempt: u32,
131 delay_ms: u64,
132 },
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct Event {
138 pub run_id: Uuid,
140
141 pub seq: u64,
143
144 pub timestamp: DateTime<Utc>,
146
147 pub kind: EventKind,
149
150 pub payload: serde_json::Value,
152}
153
154impl Event {
155 pub fn new(run_id: Uuid, seq: u64, kind: EventKind, payload: serde_json::Value) -> Self {
157 Self {
158 run_id,
159 seq,
160 timestamp: Utc::now(),
161 kind,
162 payload,
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_run_serde_roundtrip() {
173 let run = Run::new(
174 "spec_digest_123".to_string(),
175 "git_sha_abc".to_string(),
176 serde_json::json!({"question": "What is 2+2?"}),
177 );
178
179 let json = serde_json::to_string(&run).expect("serialize");
180 let deserialized: Run = serde_json::from_str(&json).expect("deserialize");
181
182 assert_eq!(run, deserialized);
183 }
184
185 #[test]
186 fn test_run_status_serde() {
187 let statuses = [
188 (RunStatus::Running, "\"RUNNING\""),
189 (RunStatus::Completed, "\"COMPLETED\""),
190 (RunStatus::Failed, "\"FAILED\""),
191 (RunStatus::Cancelled, "\"CANCELLED\""),
192 ];
193
194 for (status, expected_json) in &statuses {
195 let json = serde_json::to_string(status).expect("serialize");
196 assert_eq!(json, *expected_json);
197 let deserialized: RunStatus = serde_json::from_str(&json).expect("deserialize");
198 assert_eq!(*status, deserialized);
199 }
200 }
201
202 #[test]
203 fn test_event_serde_roundtrip_graph_started() {
204 let run_id = Uuid::new_v4();
205 let event = Event::new(run_id, 1, EventKind::GraphStarted, serde_json::json!({}));
206
207 let json = serde_json::to_string(&event).expect("serialize");
208 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
209
210 assert_eq!(event, deserialized);
211 }
212
213 #[test]
214 fn test_event_serde_roundtrip_node_entered() {
215 let run_id = Uuid::new_v4();
216 let event = Event::new(
217 run_id,
218 1,
219 EventKind::NodeEntered {
220 node_id: "node_42".to_string(),
221 iteration: 1,
222 },
223 serde_json::json!({"entry_time_ms": 100}),
224 );
225
226 let json = serde_json::to_string(&event).expect("serialize");
227 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
228
229 assert_eq!(event, deserialized);
230 }
231
232 #[test]
233 fn test_event_serde_roundtrip_tool_called() {
234 let run_id = Uuid::new_v4();
235 let event = Event::new(
236 run_id,
237 5,
238 EventKind::ToolCalled {
239 tool_name: "search".to_string(),
240 },
241 serde_json::json!({"query": "llm models"}),
242 );
243
244 let json = serde_json::to_string(&event).expect("serialize");
245 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
246
247 assert_eq!(event, deserialized);
248 }
249
250 #[test]
251 fn test_event_serde_roundtrip_checkpoint() {
252 let run_id = Uuid::new_v4();
253 let event = Event::new(
254 run_id,
255 10,
256 EventKind::CheckpointSaved {
257 checkpoint_id: "cp123".to_string(),
258 node_id: "node_x".to_string(),
259 },
260 serde_json::json!({"phase": 1, "duration_ms": 5000}),
261 );
262
263 let json = serde_json::to_string(&event).expect("serialize");
264 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
265
266 assert_eq!(event, deserialized);
267 }
268
269 #[test]
270 fn test_run_new_defaults() {
271 let inputs = serde_json::json!({"test": "data"});
272 let run = Run::new(
273 "spec_digest".to_string(),
274 "git_sha".to_string(),
275 inputs.clone(),
276 );
277
278 assert_eq!(run.status, RunStatus::Running);
279 assert!(run.finished_at.is_none());
280 assert!(run.outputs.is_none());
281 assert!(run.final_state_digest.is_none());
282 assert_eq!(run.inputs, inputs);
283 }
284}