feat: status_reason field for workflows + proper failure logging

- Add status_reason column to workflows table (migration)
- AgentUpdate::WorkflowStatus and WorkflowComplete carry reason
- Dispatch failure logs to execution_log with reason
- Worker disconnect marks orphaned workflows as failed with reason
- All status transitions now have traceable cause
This commit is contained in:
2026-04-06 20:33:41 +01:00
parent 76b964998b
commit c56bfd9377
6 changed files with 66 additions and 16 deletions

View File

@@ -158,15 +158,20 @@ impl AgentManager {
tracing::info!("Workflow {} dispatched to worker '{}'", workflow_id, name);
}
Err(e) => {
tracing::error!("Failed to dispatch workflow {}: {}", workflow_id, e);
let _ = sqlx::query("UPDATE workflows SET status = 'failed' WHERE id = ?")
.bind(&workflow_id).execute(&self.pool).await;
let _ = btx.send(WsMessage::WorkflowStatusUpdate {
workflow_id,
status: "failed".into(),
let reason = format!("调度失败: {}", e);
tracing::error!("Failed to dispatch workflow {}: {}", workflow_id, reason);
let _ = sqlx::query("UPDATE workflows SET status = 'failed', status_reason = ? WHERE id = ?")
.bind(&reason).bind(&workflow_id).execute(&self.pool).await;
// Log to execution_log so frontend can show the reason
let log_id = uuid::Uuid::new_v4().to_string();
let _ = sqlx::query(
"INSERT INTO execution_log (id, workflow_id, step_order, tool_name, tool_input, output, status, created_at) VALUES (?, ?, 0, 'system', 'dispatch', ?, 'failed', datetime('now'))"
).bind(&log_id).bind(&workflow_id).bind(&reason).execute(&self.pool).await;
let _ = btx.send(WsMessage::StepStatusUpdate {
step_id: log_id, status: "failed".into(), output: reason,
});
let _ = btx.send(WsMessage::Error {
message: format!("No worker available: {}", e),
let _ = btx.send(WsMessage::WorkflowStatusUpdate {
workflow_id, status: "failed".into(),
});
}
}
@@ -745,6 +750,7 @@ pub async fn run_step_loop(
let _ = update_tx.send(AgentUpdate::WorkflowStatus {
workflow_id: workflow_id.to_string(),
status: "waiting_user".into(),
reason: String::new(),
}).await;
send_execution(update_tx, workflow_id, step_order, "ask_user", reason, reason, "waiting").await;
@@ -789,6 +795,7 @@ pub async fn run_step_loop(
let _ = update_tx.send(AgentUpdate::WorkflowStatus {
workflow_id: workflow_id.to_string(),
status: "executing".into(),
reason: String::new(),
}).await;
let tool_msg = if feedback.is_empty() {
@@ -1084,6 +1091,7 @@ pub async fn run_agent_loop(
let _ = update_tx.send(AgentUpdate::WorkflowStatus {
workflow_id: workflow_id.to_string(),
status: "waiting_user".into(),
reason: String::new(),
}).await;
send_execution(update_tx, workflow_id, 0, "plan_approval", "等待确认计划", "等待用户确认执行计划", "waiting").await;
@@ -1114,6 +1122,7 @@ pub async fn run_agent_loop(
let _ = update_tx.send(AgentUpdate::WorkflowStatus {
workflow_id: workflow_id.to_string(),
status: "executing".into(),
reason: String::new(),
}).await;
// Stay in Planning phase, continue the loop
continue;
@@ -1130,6 +1139,7 @@ pub async fn run_agent_loop(
let _ = update_tx.send(AgentUpdate::WorkflowStatus {
workflow_id: workflow_id.to_string(),
status: "executing".into(),
reason: String::new(),
}).await;
}