use models::agent_task::TaskStatus; use serde::Serialize; use std::sync::Arc; /// Event payload published to WebSocket clients via Redis Pub/Sub. #[derive(Debug, Clone, Serialize)] pub struct TaskEvent { pub task_id: i64, pub project_id: uuid::Uuid, pub parent_id: Option, pub event: String, pub message: Option, pub output: Option, pub error: Option, pub status: String, } impl TaskEvent { pub fn started(task_id: i64, project_id: uuid::Uuid, parent_id: Option) -> Self { Self { task_id, project_id, parent_id, event: "started".to_string(), message: None, output: None, error: None, status: TaskStatus::Running.to_string(), } } pub fn progress( task_id: i64, project_id: uuid::Uuid, parent_id: Option, msg: String, ) -> Self { Self { task_id, project_id, parent_id, event: "progress".to_string(), message: Some(msg), output: None, error: None, status: TaskStatus::Running.to_string(), } } pub fn completed( task_id: i64, project_id: uuid::Uuid, parent_id: Option, output: String, ) -> Self { Self { task_id, project_id, parent_id, event: "done".to_string(), message: None, output: Some(output), error: None, status: TaskStatus::Done.to_string(), } } pub fn failed( task_id: i64, project_id: uuid::Uuid, parent_id: Option, error: String, ) -> Self { Self { task_id, project_id, parent_id, event: "failed".to_string(), message: None, output: None, error: Some(error), status: TaskStatus::Failed.to_string(), } } pub fn cancelled(task_id: i64, project_id: uuid::Uuid, parent_id: Option) -> Self { Self { task_id, project_id, parent_id, event: "cancelled".to_string(), message: None, output: None, error: None, status: TaskStatus::Cancelled.to_string(), } } } /// Helper trait for publishing task lifecycle events via Redis Pub/Sub. /// /// Callers inject a suitable `publish_fn` at construction time via /// `TaskEvents::new(...)`. If no publisher is supplied events are silently /// dropped (graceful degradation on startup). pub trait TaskEventPublisher: Send + Sync { fn publish(&self, project_id: uuid::Uuid, event: TaskEvent); } /// No-op publisher used when no Redis Pub/Sub connection is available. #[derive(Clone, Default)] pub struct NoOpPublisher; impl TaskEventPublisher for NoOpPublisher { fn publish(&self, _: uuid::Uuid, _: TaskEvent) {} } #[derive(Clone)] pub struct TaskEvents { publisher: Arc, } impl TaskEvents { pub fn new(publisher: impl TaskEventPublisher + 'static) -> Self { Self { publisher: Arc::new(publisher), } } pub fn noop() -> Self { Self::new(NoOpPublisher) } fn emit(&self, task: &models::agent_task::Model, event: TaskEvent) { self.publisher.publish(task.project_uuid, event); } pub fn emit_started(&self, task: &models::agent_task::Model) { self.emit( task, TaskEvent::started(task.id, task.project_uuid, task.parent_id), ); } pub fn emit_progress(&self, task: &models::agent_task::Model, msg: String) { self.emit( task, TaskEvent::progress(task.id, task.project_uuid, task.parent_id, msg), ); } pub fn emit_completed(&self, task: &models::agent_task::Model, output: String) { self.emit( task, TaskEvent::completed(task.id, task.project_uuid, task.parent_id, output), ); } pub fn emit_failed(&self, task: &models::agent_task::Model, error: String) { self.emit( task, TaskEvent::failed(task.id, task.project_uuid, task.parent_id, error), ); } pub fn emit_cancelled(&self, task: &models::agent_task::Model) { self.emit( task, TaskEvent::cancelled(task.id, task.project_uuid, task.parent_id), ); } }