Skip to main content

ironflow_store/entities/
step.rs

1//! [`Step`] entity and related request/update types.
2
3use 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
11/// A single operation within a run.
12///
13/// Steps are executed sequentially in order of [`position`](Step::position).
14///
15/// # Examples
16///
17/// ```
18/// use ironflow_store::entities::Step;
19///
20/// // Steps are created by RunStore::create_step, not directly.
21/// ```
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[non_exhaustive]
24pub struct Step {
25    /// Unique identifier (UUIDv7).
26    pub id: Uuid,
27    /// The run this step belongs to.
28    pub run_id: Uuid,
29    /// Human-readable step name (e.g. "build", "test", "review").
30    pub name: String,
31    /// The type of operation.
32    pub kind: StepKind,
33    /// Execution wave within the run (0-based).
34    ///
35    /// In linear flows, this strictly increases (0, 1, 2, ...).
36    /// In DAGs with parallel execution, steps at the same wave share
37    /// the same position and execute concurrently. Use
38    /// `step_dependencies` to determine the actual execution order.
39    pub position: u32,
40    /// Current FSM status — embeds state + state_machine_id for SQL-side transitions.
41    pub status: FsmState<StepStatus>,
42    /// Serialized operation configuration.
43    pub input: Option<Value>,
44    /// Serialized operation output.
45    pub output: Option<Value>,
46    /// Error message if the step failed.
47    pub error: Option<String>,
48    /// Wall-clock execution duration in milliseconds.
49    pub duration_ms: u64,
50    /// Cost in USD (agent steps only).
51    pub cost_usd: Decimal,
52    /// Input token count (agent steps only).
53    pub input_tokens: Option<u64>,
54    /// Output token count (agent steps only).
55    pub output_tokens: Option<u64>,
56    /// When the step was created.
57    pub created_at: DateTime<Utc>,
58    /// When the step record was last updated.
59    pub updated_at: DateTime<Utc>,
60    /// When step execution started.
61    pub started_at: Option<DateTime<Utc>>,
62    /// When step execution finished.
63    pub completed_at: Option<DateTime<Utc>>,
64    /// Debug messages (verbose conversation trace), stored as JSON.
65    pub debug_messages: Option<Value>,
66}
67
68/// Request to create a new step.
69///
70/// # Examples
71///
72/// ```
73/// use ironflow_store::entities::{NewStep, StepKind};
74/// use serde_json::json;
75/// use uuid::Uuid;
76///
77/// let req = NewStep {
78///     run_id: Uuid::nil(),
79///     name: "build".to_string(),
80///     kind: StepKind::Shell,
81///     position: 0,
82///     input: Some(json!({"command": "cargo build"})),
83/// };
84/// ```
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct NewStep {
87    /// The run this step belongs to.
88    pub run_id: Uuid,
89    /// Step name.
90    pub name: String,
91    /// Operation type.
92    pub kind: StepKind,
93    /// Execution order (0-based).
94    pub position: u32,
95    /// Serialized operation configuration.
96    pub input: Option<Value>,
97}
98
99/// Partial update for a step after execution.
100///
101/// Only `Some` fields are applied; `None` fields are left unchanged.
102///
103/// # Examples
104///
105/// ```
106/// use ironflow_store::entities::{StepUpdate, StepStatus};
107/// use serde_json::json;
108///
109/// let update = StepUpdate {
110///     status: Some(StepStatus::Completed),
111///     output: Some(json!({"stdout": "ok"})),
112///     ..StepUpdate::default()
113/// };
114/// ```
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct StepUpdate {
117    /// New status.
118    pub status: Option<StepStatus>,
119    /// Operation output.
120    pub output: Option<Value>,
121    /// Error message.
122    pub error: Option<String>,
123    /// Execution duration.
124    pub duration_ms: Option<u64>,
125    /// Cost in USD.
126    pub cost_usd: Option<Decimal>,
127    /// Input token count.
128    pub input_tokens: Option<u64>,
129    /// Output token count.
130    pub output_tokens: Option<u64>,
131    /// When execution started.
132    pub started_at: Option<DateTime<Utc>>,
133    /// When execution completed.
134    pub completed_at: Option<DateTime<Utc>>,
135    /// Debug messages (verbose conversation trace), stored as JSON.
136    pub debug_messages: Option<Value>,
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use serde_json::json;
143
144    #[test]
145    fn newstep_serde_roundtrip() {
146        let new_step = NewStep {
147            run_id: Uuid::nil(),
148            name: "build".to_string(),
149            kind: StepKind::Shell,
150            position: 0,
151            input: Some(json!({"command": "cargo build"})),
152        };
153
154        let json = serde_json::to_string(&new_step).expect("serialize");
155        let back: NewStep = serde_json::from_str(&json).expect("deserialize");
156
157        assert_eq!(back.run_id, new_step.run_id);
158        assert_eq!(back.name, new_step.name);
159        assert_eq!(back.kind, new_step.kind);
160        assert_eq!(back.position, new_step.position);
161        assert_eq!(back.input, new_step.input);
162    }
163
164    #[test]
165    fn step_serde_preserves_all_fields() {
166        use crate::entities::FsmState;
167        use chrono::Utc;
168
169        let now = Utc::now();
170        let step = Step {
171            id: Uuid::now_v7(),
172            run_id: Uuid::now_v7(),
173            name: "test-step".to_string(),
174            kind: StepKind::Agent,
175            position: 1,
176            status: FsmState::new(StepStatus::Completed, Uuid::now_v7()),
177            input: Some(json!({"input": "data"})),
178            output: Some(json!({"output": "result"})),
179            error: None,
180            duration_ms: 2500,
181            cost_usd: Decimal::new(150, 2),
182            input_tokens: Some(100),
183            output_tokens: Some(200),
184            created_at: now,
185            updated_at: now,
186            started_at: Some(now),
187            completed_at: Some(now),
188            debug_messages: None,
189        };
190
191        let json = serde_json::to_string(&step).expect("serialize");
192        let back: Step = serde_json::from_str(&json).expect("deserialize");
193
194        assert_eq!(back.id, step.id);
195        assert_eq!(back.run_id, step.run_id);
196        assert_eq!(back.name, step.name);
197        assert_eq!(back.kind, step.kind);
198        assert_eq!(back.position, step.position);
199        assert_eq!(back.status.state, step.status.state);
200        assert_eq!(back.input, step.input);
201        assert_eq!(back.output, step.output);
202        assert_eq!(back.error, step.error);
203        assert_eq!(back.duration_ms, step.duration_ms);
204        assert_eq!(back.cost_usd, step.cost_usd);
205        assert_eq!(back.input_tokens, step.input_tokens);
206        assert_eq!(back.output_tokens, step.output_tokens);
207    }
208
209    #[test]
210    fn stepupdate_default_is_no_changes() {
211        let update = StepUpdate::default();
212        assert!(update.status.is_none());
213        assert!(update.output.is_none());
214        assert!(update.error.is_none());
215        assert!(update.duration_ms.is_none());
216        assert!(update.cost_usd.is_none());
217        assert!(update.input_tokens.is_none());
218        assert!(update.output_tokens.is_none());
219        assert!(update.started_at.is_none());
220        assert!(update.completed_at.is_none());
221        assert!(update.debug_messages.is_none());
222    }
223
224    #[test]
225    fn stepupdate_serde_roundtrip() {
226        let update = StepUpdate {
227            status: Some(StepStatus::Completed),
228            output: Some(json!({"result": "ok"})),
229            error: None,
230            duration_ms: Some(1000),
231            cost_usd: Some(Decimal::new(50, 2)),
232            input_tokens: Some(50),
233            output_tokens: Some(75),
234            started_at: None,
235            completed_at: None,
236            debug_messages: None,
237        };
238
239        let json = serde_json::to_string(&update).expect("serialize");
240        let back: StepUpdate = serde_json::from_str(&json).expect("deserialize");
241
242        assert_eq!(back.status, update.status);
243        assert_eq!(back.output, update.output);
244        assert_eq!(back.duration_ms, update.duration_ms);
245        assert_eq!(back.cost_usd, update.cost_usd);
246        assert_eq!(back.input_tokens, update.input_tokens);
247        assert_eq!(back.output_tokens, update.output_tokens);
248    }
249}