@@ -136,6 +136,8 @@ pub struct WorkflowStatus {
136136 pub result : Option < serde_json:: Value > ,
137137 /// Error description; absent unless the workflow failed.
138138 pub error : Option < String > ,
139+ /// Ordered list of steps recorded during execution.
140+ pub steps : Vec < WorkflowStep > ,
139141}
140142
141143/// Lifecycle phases of a workflow run.
@@ -154,6 +156,35 @@ pub enum ExecutionStatus {
154156 Cancelled ,
155157}
156158
159+ /// Terminal state of a single workflow step.
160+ #[ derive( Debug , Clone , Serialize ) ]
161+ #[ serde( rename_all = "snake_case" ) ]
162+ pub enum StepStatus {
163+ /// Step is currently executing.
164+ Running ,
165+ /// Step finished without error.
166+ Completed ,
167+ /// Step aborted due to an error.
168+ Failed ,
169+ }
170+
171+ /// A recorded execution step within a workflow run.
172+ #[ derive( Debug , Clone , Serialize ) ]
173+ pub struct WorkflowStep {
174+ /// Step identifier, unique within a workflow run.
175+ pub id : String ,
176+ /// Human-readable step name.
177+ pub name : String ,
178+ /// Terminal lifecycle state of this step.
179+ pub status : StepStatus ,
180+ /// UTC timestamp when this step started.
181+ pub started_at : chrono:: DateTime < chrono:: Utc > ,
182+ /// UTC timestamp when this step finished; absent while the step is running.
183+ pub completed_at : Option < chrono:: DateTime < chrono:: Utc > > ,
184+ /// Output produced by this step; absent when not available.
185+ pub output : Option < String > ,
186+ }
187+
157188/// Event broadcast over WebSocket to connected clients.
158189#[ derive( Debug , Clone , Serialize ) ]
159190pub struct WebSocketMessage {
@@ -225,11 +256,10 @@ async fn get_execution_trace(
225256 let sessions = state. workflow_sessions . read ( ) . await ;
226257
227258 if let Some ( status) = sessions. get ( & id) {
228- // Return detailed execution trace
229259 let trace = serde_json:: json!( {
230260 "workflow_id" : id,
231261 "status" : status. status,
232- "steps" : [ ] , // TODO: Implement detailed step tracking
262+ "steps" : status . steps ,
233263 "timeline" : {
234264 "started_at" : status. started_at,
235265 "completed_at" : status. completed_at
@@ -248,6 +278,107 @@ async fn get_execution_trace(
248278 }
249279}
250280
281+ /// Appends a completed step record to an existing workflow run.
282+ pub async fn record_workflow_step (
283+ sessions : & WorkflowSessions ,
284+ workflow_id : & str ,
285+ step : WorkflowStep ,
286+ ) {
287+ let mut sessions = sessions. write ( ) . await ;
288+ if let Some ( workflow) = sessions. get_mut ( workflow_id) {
289+ workflow. steps . push ( step) ;
290+ }
291+ }
292+
293+ #[ cfg( test) ]
294+ mod tests {
295+ use super :: * ;
296+
297+ #[ tokio:: test]
298+ async fn trace_includes_recorded_steps ( ) {
299+ let sessions = RwLock :: new ( HashMap :: new ( ) ) ;
300+ let ( broadcaster, _rx) = broadcast:: channel ( 16 ) ;
301+ let wf_id = "wf_test_123" . to_string ( ) ;
302+
303+ create_workflow_session (
304+ & sessions,
305+ & broadcaster,
306+ wf_id. clone ( ) ,
307+ "test_pattern" . to_string ( ) ,
308+ )
309+ . await ;
310+
311+ let step = WorkflowStep {
312+ id : "step_1" . to_string ( ) ,
313+ name : "Parse Input" . to_string ( ) ,
314+ status : StepStatus :: Completed ,
315+ started_at : chrono:: Utc :: now ( ) ,
316+ completed_at : Some ( chrono:: Utc :: now ( ) ) ,
317+ output : Some ( "parsed successfully" . to_string ( ) ) ,
318+ } ;
319+ record_workflow_step ( & sessions, & wf_id, step) . await ;
320+
321+ let guard = sessions. read ( ) . await ;
322+ let wf = guard. get ( & wf_id) . expect ( "workflow should exist" ) ;
323+ assert_eq ! ( wf. steps. len( ) , 1 ) ;
324+ assert_eq ! ( wf. steps[ 0 ] . name, "Parse Input" ) ;
325+ assert_eq ! ( wf. steps[ 0 ] . id, "step_1" ) ;
326+ assert ! ( matches!( wf. steps[ 0 ] . status, StepStatus :: Completed ) ) ;
327+ }
328+
329+ #[ tokio:: test]
330+ async fn trace_starts_with_empty_steps ( ) {
331+ let sessions = RwLock :: new ( HashMap :: new ( ) ) ;
332+ let ( broadcaster, _rx) = broadcast:: channel ( 16 ) ;
333+ let wf_id = "wf_empty_456" . to_string ( ) ;
334+
335+ create_workflow_session (
336+ & sessions,
337+ & broadcaster,
338+ wf_id. clone ( ) ,
339+ "test_pattern" . to_string ( ) ,
340+ )
341+ . await ;
342+
343+ let guard = sessions. read ( ) . await ;
344+ let wf = guard. get ( & wf_id) . expect ( "workflow should exist" ) ;
345+ assert ! ( wf. steps. is_empty( ) ) ;
346+ }
347+
348+ #[ tokio:: test]
349+ async fn record_multiple_steps_preserves_order ( ) {
350+ let sessions = RwLock :: new ( HashMap :: new ( ) ) ;
351+ let ( broadcaster, _rx) = broadcast:: channel ( 16 ) ;
352+ let wf_id = "wf_multi_789" . to_string ( ) ;
353+
354+ create_workflow_session (
355+ & sessions,
356+ & broadcaster,
357+ wf_id. clone ( ) ,
358+ "test_pattern" . to_string ( ) ,
359+ )
360+ . await ;
361+
362+ for i in 1 ..=3u32 {
363+ let step = WorkflowStep {
364+ id : format ! ( "step_{i}" ) ,
365+ name : format ! ( "Step {i}" ) ,
366+ status : StepStatus :: Completed ,
367+ started_at : chrono:: Utc :: now ( ) ,
368+ completed_at : Some ( chrono:: Utc :: now ( ) ) ,
369+ output : None ,
370+ } ;
371+ record_workflow_step ( & sessions, & wf_id, step) . await ;
372+ }
373+
374+ let guard = sessions. read ( ) . await ;
375+ let wf = guard. get ( & wf_id) . expect ( "workflow should exist" ) ;
376+ assert_eq ! ( wf. steps. len( ) , 3 ) ;
377+ assert_eq ! ( wf. steps[ 0 ] . name, "Step 1" ) ;
378+ assert_eq ! ( wf. steps[ 2 ] . name, "Step 3" ) ;
379+ }
380+ }
381+
251382async fn list_workflows ( State ( state) : State < AppState > ) -> Json < Vec < WorkflowStatus > > {
252383 let sessions = state. workflow_sessions . read ( ) . await ;
253384 let workflows: Vec < WorkflowStatus > = sessions. values ( ) . cloned ( ) . collect ( ) ;
@@ -312,6 +443,7 @@ pub async fn create_workflow_session(
312443 completed_at : None ,
313444 result : None ,
314445 error : None ,
446+ steps : vec ! [ ] ,
315447 } ;
316448
317449 sessions. write ( ) . await . insert ( workflow_id. clone ( ) , status) ;
0 commit comments