Skip to content

Commit e3231f8

Browse files
author
Test User
committed
Merge remote-tracking branch 'gitea/task/2503-workflow-step-tracking'
2 parents a667e59 + 4a81b9a commit e3231f8

3 files changed

Lines changed: 158 additions & 5 deletions

File tree

terraphim_server/src/workflows/mod.rs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
159190
pub 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+
251382
async 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);

terraphim_server/src/workflows/multi_agent_handlers.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use terraphim_persistence::DeviceStorage;
1616
use terraphim_types::RelevanceFunction;
1717

1818
use super::{
19-
ExecutionStatus, LlmConfig, StepConfig, WebSocketBroadcaster, WorkflowSessions,
20-
update_workflow_status,
19+
ExecutionStatus, LlmConfig, StepConfig, StepStatus, WebSocketBroadcaster, WorkflowSessions,
20+
WorkflowStep, record_workflow_step, update_workflow_status,
2121
};
2222
use terraphim_config::ConfigState;
2323

@@ -258,9 +258,14 @@ impl MultiAgentWorkflowExecutor {
258258

259259
let input = CommandInput::new(step_prompt, CommandType::Generate);
260260

261+
// Capture step start time before executing
262+
let step_started_at = chrono::Utc::now();
263+
261264
// Execute with specialized agent
262265
let output = step_agent.process_command(input).await?;
263266

267+
let step_completed_at = chrono::Utc::now();
268+
264269
// Update context for next step (prompt chaining)
265270
context = format!(
266271
"{}\n\nStep {} ({}): {}",
@@ -277,7 +282,7 @@ impl MultiAgentWorkflowExecutor {
277282
"role": role,
278283
"overall_role": overall_role,
279284
"output": output.text,
280-
"duration_ms": 2000, // Real execution time would be tracked
285+
"duration_ms": (step_completed_at - step_started_at).num_milliseconds(),
281286
"success": true,
282287
"agent_id": step_agent.agent_id.to_string(),
283288
"tokens_used": {
@@ -286,6 +291,21 @@ impl MultiAgentWorkflowExecutor {
286291
}
287292
});
288293

294+
// Record step in workflow status for the trace endpoint
295+
record_workflow_step(
296+
sessions,
297+
workflow_id,
298+
WorkflowStep {
299+
id: step_id.clone(),
300+
name: step_name.clone(),
301+
status: StepStatus::Completed,
302+
started_at: step_started_at,
303+
completed_at: Some(step_completed_at),
304+
output: Some(output.text[..std::cmp::min(500, output.text.len())].to_string()),
305+
},
306+
)
307+
.await;
308+
289309
results.push(step_result);
290310

291311
// Small delay for progress updates

terraphim_server/tests/simple_workflow_test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async fn test_workflow_system_basic() {
2424
completed_at: None,
2525
result: None,
2626
error: None,
27+
steps: vec![],
2728
},
2829
);
2930
}

0 commit comments

Comments
 (0)