1use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9use super::{FsmState, StepKind, StepStatus};
10
11fn default_attempt() -> u32 {
14 1
15}
16
17pub fn step_trace_id(run_id: Uuid, name: &str, position: u32) -> Uuid {
38 Uuid::new_v5(
39 &Uuid::NAMESPACE_OID,
40 format!("{run_id}:{name}:{position}").as_bytes(),
41 )
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
56#[non_exhaustive]
57pub struct Step {
58 pub id: Uuid,
60 pub trace_id: Uuid,
65 pub run_id: Uuid,
67 pub name: String,
69 pub kind: StepKind,
71 pub position: u32,
78 pub status: FsmState<StepStatus>,
80 #[serde(default = "default_attempt")]
86 pub attempt: u32,
87 pub input: Option<Value>,
89 pub output: Option<Value>,
91 pub error: Option<String>,
93 pub duration_ms: u64,
95 pub cost_usd: Decimal,
97 pub input_tokens: Option<u64>,
99 pub output_tokens: Option<u64>,
101 pub created_at: DateTime<Utc>,
103 pub updated_at: DateTime<Utc>,
105 pub started_at: Option<DateTime<Utc>>,
107 pub completed_at: Option<DateTime<Utc>>,
109 pub debug_messages: Option<Value>,
111 #[serde(default)]
113 pub is_error_handler: bool,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct NewStep {
138 pub run_id: Uuid,
140 pub trace_id: Uuid,
142 pub name: String,
144 pub kind: StepKind,
146 pub position: u32,
148 pub input: Option<Value>,
150 #[serde(default)]
152 pub is_error_handler: bool,
153}
154
155#[derive(Debug, Clone, Default, Serialize, Deserialize)]
172pub struct StepUpdate {
173 pub status: Option<StepStatus>,
175 pub output: Option<Value>,
177 pub error: Option<String>,
179 pub duration_ms: Option<u64>,
181 pub cost_usd: Option<Decimal>,
183 pub input_tokens: Option<u64>,
185 pub output_tokens: Option<u64>,
187 pub started_at: Option<DateTime<Utc>>,
189 pub completed_at: Option<DateTime<Utc>>,
191 pub debug_messages: Option<Value>,
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use serde_json::json;
199
200 #[test]
201 fn newstep_serde_roundtrip() {
202 let new_step = NewStep {
203 run_id: Uuid::nil(),
204 trace_id: step_trace_id(Uuid::nil(), "build", 0),
205 name: "build".to_string(),
206 kind: StepKind::Shell,
207 position: 0,
208 input: Some(json!({"command": "cargo build"})),
209 is_error_handler: false,
210 };
211
212 let json = serde_json::to_string(&new_step).expect("serialize");
213 let back: NewStep = serde_json::from_str(&json).expect("deserialize");
214
215 assert_eq!(back.run_id, new_step.run_id);
216 assert_eq!(back.name, new_step.name);
217 assert_eq!(back.kind, new_step.kind);
218 assert_eq!(back.position, new_step.position);
219 assert_eq!(back.input, new_step.input);
220 }
221
222 #[test]
223 fn step_serde_preserves_all_fields() {
224 use crate::entities::FsmState;
225 use chrono::Utc;
226
227 let now = Utc::now();
228 let run_id = Uuid::now_v7();
229 let step = Step {
230 id: Uuid::now_v7(),
231 trace_id: step_trace_id(run_id, "test-step", 1),
232 run_id,
233 name: "test-step".to_string(),
234 kind: StepKind::Agent,
235 position: 1,
236 status: FsmState::new(StepStatus::Completed, Uuid::now_v7()),
237 attempt: 2,
238 input: Some(json!({"input": "data"})),
239 output: Some(json!({"output": "result"})),
240 error: None,
241 duration_ms: 2500,
242 cost_usd: Decimal::new(150, 2),
243 input_tokens: Some(100),
244 output_tokens: Some(200),
245 created_at: now,
246 updated_at: now,
247 started_at: Some(now),
248 completed_at: Some(now),
249 debug_messages: None,
250 is_error_handler: false,
251 };
252
253 let json = serde_json::to_string(&step).expect("serialize");
254 let back: Step = serde_json::from_str(&json).expect("deserialize");
255
256 assert_eq!(back.id, step.id);
257 assert_eq!(back.run_id, step.run_id);
258 assert_eq!(back.name, step.name);
259 assert_eq!(back.kind, step.kind);
260 assert_eq!(back.position, step.position);
261 assert_eq!(back.status.state, step.status.state);
262 assert_eq!(back.attempt, step.attempt);
263 assert_eq!(back.input, step.input);
264 assert_eq!(back.output, step.output);
265 assert_eq!(back.error, step.error);
266 assert_eq!(back.duration_ms, step.duration_ms);
267 assert_eq!(back.cost_usd, step.cost_usd);
268 assert_eq!(back.input_tokens, step.input_tokens);
269 assert_eq!(back.output_tokens, step.output_tokens);
270 }
271
272 #[test]
273 fn stepupdate_default_is_no_changes() {
274 let update = StepUpdate::default();
275 assert!(update.status.is_none());
276 assert!(update.output.is_none());
277 assert!(update.error.is_none());
278 assert!(update.duration_ms.is_none());
279 assert!(update.cost_usd.is_none());
280 assert!(update.input_tokens.is_none());
281 assert!(update.output_tokens.is_none());
282 assert!(update.started_at.is_none());
283 assert!(update.completed_at.is_none());
284 assert!(update.debug_messages.is_none());
285 }
286
287 #[test]
288 fn stepupdate_serde_roundtrip() {
289 let update = StepUpdate {
290 status: Some(StepStatus::Completed),
291 output: Some(json!({"result": "ok"})),
292 error: None,
293 duration_ms: Some(1000),
294 cost_usd: Some(Decimal::new(50, 2)),
295 input_tokens: Some(50),
296 output_tokens: Some(75),
297 started_at: None,
298 completed_at: None,
299 debug_messages: None,
300 };
301
302 let json = serde_json::to_string(&update).expect("serialize");
303 let back: StepUpdate = serde_json::from_str(&json).expect("deserialize");
304
305 assert_eq!(back.status, update.status);
306 assert_eq!(back.output, update.output);
307 assert_eq!(back.duration_ms, update.duration_ms);
308 assert_eq!(back.cost_usd, update.cost_usd);
309 assert_eq!(back.input_tokens, update.input_tokens);
310 assert_eq!(back.output_tokens, update.output_tokens);
311 }
312
313 #[test]
314 fn trace_id_is_deterministic() {
315 let run_id = Uuid::nil();
316 let id1 = step_trace_id(run_id, "build", 0);
317 let id2 = step_trace_id(run_id, "build", 0);
318 assert_eq!(id1, id2);
319 }
320
321 #[test]
322 fn trace_id_differs_for_different_inputs() {
323 let run_id = Uuid::nil();
324 let a = step_trace_id(run_id, "build", 0);
325 let b = step_trace_id(run_id, "test", 0);
326 let c = step_trace_id(run_id, "build", 1);
327 let d = step_trace_id(Uuid::max(), "build", 0);
328
329 assert_ne!(a, b);
330 assert_ne!(a, c);
331 assert_ne!(a, d);
332 }
333
334 #[test]
335 fn trace_id_is_uuid_v5() {
336 let id = step_trace_id(Uuid::nil(), "build", 0);
337 assert_eq!(id.get_version_num(), 5);
338 }
339}