1use std::collections::HashMap;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11pub use pe_core::scope::TaskId;
13
14#[must_use]
16pub fn new_task_id() -> TaskId {
17 uuid::Uuid::new_v4().to_string()
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct Task {
38 pub id: TaskId,
40 pub task_type: TaskType,
42
43 pub title: String,
46 pub description: String,
48 pub status: TaskStatus,
50 pub priority: TaskPriority,
52 pub tags: Vec<String>,
54
55 pub parent_task_id: Option<TaskId>,
58
59 pub agent_id: Option<String>,
62 pub created_by: String,
64 pub assignee: String,
66
67 pub result: Option<serde_json::Value>,
70 pub error: Option<String>,
72
73 pub metadata: HashMap<String, serde_json::Value>,
77
78 pub created_at: DateTime<Utc>,
80 pub updated_at: Option<DateTime<Utc>>,
81 pub completed_at: Option<DateTime<Utc>>,
82 pub deleted_at: Option<DateTime<Utc>>,
84}
85
86impl Task {
87 #[must_use]
89 pub fn new(title: impl Into<String>) -> Self {
90 Self {
91 id: new_task_id(),
92 task_type: TaskType::Human,
93 title: title.into(),
94 description: String::new(),
95 status: TaskStatus::Pending,
96 priority: TaskPriority::Medium,
97 tags: Vec::new(),
98 parent_task_id: None,
99 agent_id: None,
100 created_by: "user".into(),
101 assignee: "user".into(),
102 result: None,
103 error: None,
104 metadata: HashMap::new(),
105 created_at: Utc::now(),
106 updated_at: None,
107 completed_at: None,
108 deleted_at: None,
109 }
110 }
111
112 #[must_use]
114 pub fn agent_task(title: impl Into<String>, agent_id: impl Into<String>) -> Self {
115 let aid = agent_id.into();
116 Self {
117 task_type: TaskType::Agent,
118 agent_id: Some(aid.clone()),
119 created_by: aid.clone(),
120 assignee: aid,
121 ..Self::new(title)
122 }
123 }
124
125 #[must_use]
127 pub fn plan(title: impl Into<String>) -> Self {
128 Self {
129 task_type: TaskType::Plan,
130 created_by: "system".into(),
131 assignee: "system".into(),
132 ..Self::new(title)
133 }
134 }
135
136 #[must_use]
138 pub fn is_deleted(&self) -> bool {
139 self.deleted_at.is_some()
140 }
141
142 #[must_use]
144 pub fn is_terminal(&self) -> bool {
145 matches!(self.status, TaskStatus::Completed | TaskStatus::Cancelled)
146 }
147
148 #[must_use]
150 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
151 self.description = desc.into();
152 self
153 }
154
155 #[must_use]
157 pub fn with_priority(mut self, priority: TaskPriority) -> Self {
158 self.priority = priority;
159 self
160 }
161
162 #[must_use]
164 pub fn with_parent(mut self, parent_id: impl Into<TaskId>) -> Self {
165 self.parent_task_id = Some(parent_id.into());
166 self
167 }
168
169 #[must_use]
171 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
172 self.tags = tags;
173 self
174 }
175}
176
177#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
181#[non_exhaustive]
182pub enum TaskStatus {
183 Pending,
185 InProgress,
187 Completed,
189 Failed,
191 Blocked,
193 Cancelled,
195}
196
197#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
199#[non_exhaustive]
200pub enum TaskType {
201 System,
203 Agent,
205 Human,
207 Plan,
209}
210
211#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
213#[non_exhaustive]
214#[derive(Default)]
215pub enum TaskPriority {
216 Urgent = 1,
218 High = 2,
220 #[default]
222 Medium = 3,
223 Low = 4,
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_new_task_defaults() {
233 let t = Task::new("Build feature");
234 assert_eq!(t.title, "Build feature");
235 assert_eq!(t.status, TaskStatus::Pending);
236 assert_eq!(t.priority, TaskPriority::Medium);
237 assert_eq!(t.task_type, TaskType::Human);
238 assert_eq!(t.created_by, "user");
239 assert_eq!(t.assignee, "user");
240 assert!(t.parent_task_id.is_none());
241 assert!(!t.id.is_empty());
242 }
243
244 #[test]
245 fn test_agent_task() {
246 let t = Task::agent_task("Research APIs", "agent-1");
247 assert_eq!(t.task_type, TaskType::Agent);
248 assert_eq!(t.agent_id.as_deref(), Some("agent-1"));
249 assert_eq!(t.created_by, "agent-1");
250 assert_eq!(t.assignee, "agent-1");
251 }
252
253 #[test]
254 fn test_plan_task() {
255 let t = Task::plan("Deploy v2.0");
256 assert_eq!(t.task_type, TaskType::Plan);
257 assert_eq!(t.created_by, "system");
258 }
259
260 #[test]
261 fn test_builder_chain() {
262 let t = Task::new("Fix bug")
263 .with_description("The login form crashes")
264 .with_priority(TaskPriority::High)
265 .with_tags(vec!["bug".into(), "login".into()]);
266 assert_eq!(t.description, "The login form crashes");
267 assert_eq!(t.priority, TaskPriority::High);
268 assert_eq!(t.tags.len(), 2);
269 }
270
271 #[test]
272 fn test_subtask_with_parent() {
273 let parent = Task::plan("Big project");
274 let child = Task::new("Step 1").with_parent(&parent.id);
275 assert_eq!(child.parent_task_id.as_deref(), Some(parent.id.as_str()));
276 }
277
278 #[test]
279 fn test_serde_roundtrip() {
280 let t = Task::new("Test task");
281 let json = serde_json::to_string(&t).unwrap();
282 let t2: Task = serde_json::from_str(&json).unwrap();
283 assert_eq!(t.id, t2.id);
284 assert_eq!(t.title, t2.title);
285 }
286
287 #[test]
288 fn test_is_terminal() {
289 let mut t = Task::new("Done");
290 assert!(!t.is_terminal());
291 t.status = TaskStatus::Completed;
292 assert!(t.is_terminal());
293 t.status = TaskStatus::Cancelled;
294 assert!(t.is_terminal());
295 t.status = TaskStatus::Failed;
296 assert!(!t.is_terminal()); }
298}