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,
74
75 GraphFailed,
77
78 #[serde(rename = "node_entered")]
80 NodeEntered { node_id: String },
81
82 #[serde(rename = "node_exited")]
84 NodeExited { node_id: String },
85
86 #[serde(rename = "node_failed")]
88 NodeFailed { node_id: String },
89
90 #[serde(rename = "tool_called")]
92 ToolCalled { tool_name: String },
93
94 #[serde(rename = "tool_returned")]
96 ToolReturned { tool_name: String },
97
98 #[serde(rename = "tool_failed")]
100 ToolFailed { tool_name: String },
101
102 #[serde(rename = "checkpoint")]
104 Checkpoint { label: String },
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct Event {
110 pub run_id: Uuid,
112
113 pub seq: u64,
115
116 pub timestamp: DateTime<Utc>,
118
119 pub kind: EventKind,
121
122 pub payload: serde_json::Value,
124}
125
126impl Event {
127 pub fn new(run_id: Uuid, seq: u64, kind: EventKind, payload: serde_json::Value) -> Self {
129 Self {
130 run_id,
131 seq,
132 timestamp: Utc::now(),
133 kind,
134 payload,
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_run_serde_roundtrip() {
145 let run = Run::new(
146 "spec_digest_123".to_string(),
147 "git_sha_abc".to_string(),
148 serde_json::json!({"question": "What is 2+2?"}),
149 );
150
151 let json = serde_json::to_string(&run).expect("serialize");
152 let deserialized: Run = serde_json::from_str(&json).expect("deserialize");
153
154 assert_eq!(run, deserialized);
155 }
156
157 #[test]
158 fn test_run_status_serde() {
159 let statuses = [
160 RunStatus::Running,
161 RunStatus::Completed,
162 RunStatus::Failed,
163 RunStatus::Cancelled,
164 ];
165
166 for status in &statuses {
167 let json = serde_json::to_string(status).expect("serialize");
168 let deserialized: RunStatus = serde_json::from_str(&json).expect("deserialize");
169 assert_eq!(*status, deserialized);
170 }
171 }
172
173 #[test]
174 fn test_event_serde_roundtrip_graph_started() {
175 let run_id = Uuid::new_v4();
176 let event = Event::new(run_id, 0, EventKind::GraphStarted, serde_json::json!({}));
177
178 let json = serde_json::to_string(&event).expect("serialize");
179 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
180
181 assert_eq!(event, deserialized);
182 }
183
184 #[test]
185 fn test_event_serde_roundtrip_node_entered() {
186 let run_id = Uuid::new_v4();
187 let event = Event::new(
188 run_id,
189 1,
190 EventKind::NodeEntered {
191 node_id: "node_42".to_string(),
192 },
193 serde_json::json!({"entry_time_ms": 100}),
194 );
195
196 let json = serde_json::to_string(&event).expect("serialize");
197 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
198
199 assert_eq!(event, deserialized);
200 }
201
202 #[test]
203 fn test_event_serde_roundtrip_tool_called() {
204 let run_id = Uuid::new_v4();
205 let event = Event::new(
206 run_id,
207 5,
208 EventKind::ToolCalled {
209 tool_name: "search".to_string(),
210 },
211 serde_json::json!({"query": "llm models"}),
212 );
213
214 let json = serde_json::to_string(&event).expect("serialize");
215 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
216
217 assert_eq!(event, deserialized);
218 }
219
220 #[test]
221 fn test_event_serde_roundtrip_checkpoint() {
222 let run_id = Uuid::new_v4();
223 let event = Event::new(
224 run_id,
225 10,
226 EventKind::Checkpoint {
227 label: "phase_1_complete".to_string(),
228 },
229 serde_json::json!({"phase": 1, "duration_ms": 5000}),
230 );
231
232 let json = serde_json::to_string(&event).expect("serialize");
233 let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
234
235 assert_eq!(event, deserialized);
236 }
237
238 #[test]
239 fn test_run_new_defaults() {
240 let inputs = serde_json::json!({"test": "data"});
241 let run = Run::new(
242 "spec_digest".to_string(),
243 "git_sha".to_string(),
244 inputs.clone(),
245 );
246
247 assert_eq!(run.status, RunStatus::Running);
248 assert!(run.finished_at.is_none());
249 assert!(run.outputs.is_none());
250 assert!(run.final_state_digest.is_none());
251 assert_eq!(run.inputs, inputs);
252 }
253}