Skip to main content

pe_tasks/
task.rs

1//! # Core task types — the structural primitives.
2//!
3//! A `Task` is a typed work item that agents create, track, decompose, and
4//! depend on. The library provides the structure; users build policies on top.
5
6use std::collections::HashMap;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11/// Unique identifier for a task. Re-exported from pe-core for consistency.
12pub use pe_core::scope::TaskId;
13
14/// Generate a new unique task ID.
15#[must_use]
16pub fn new_task_id() -> TaskId {
17    uuid::Uuid::new_v4().to_string()
18}
19
20/// A structured work item.
21///
22/// Tasks are the atomic unit of work in the agent system. They can be
23/// organized hierarchically (parent/child via `parent_task_id`), linked
24/// via dependencies (see [`TaskDependency`](crate::dependency::TaskDependency)),
25/// and tracked through lifecycle transitions.
26///
27/// # Example
28///
29/// ```
30/// use pe_tasks::Task;
31///
32/// let task = Task::new("Implement login page");
33/// assert_eq!(task.status, pe_tasks::TaskStatus::Pending);
34/// assert_eq!(task.priority, pe_tasks::TaskPriority::Medium);
35/// ```
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct Task {
38    /// Unique identifier.
39    pub id: TaskId,
40    /// Discriminator: who/what created this task.
41    pub task_type: TaskType,
42
43    // --- Content ---
44    /// Short title (what needs to be done).
45    pub title: String,
46    /// Detailed description (how/why).
47    pub description: String,
48    /// Current status in the lifecycle.
49    pub status: TaskStatus,
50    /// Priority level (1=urgent, 4=low).
51    pub priority: TaskPriority,
52    /// Freeform tags for classification.
53    pub tags: Vec<String>,
54
55    // --- Hierarchy ---
56    /// Parent task ID for subtask relationships. None = root task.
57    pub parent_task_id: Option<TaskId>,
58
59    // --- Ownership ---
60    /// Agent that created or owns this task. None for human-created tasks.
61    pub agent_id: Option<String>,
62    /// Who created this task: "user", "system", or an agent ID.
63    pub created_by: String,
64    /// Who is responsible: "user" or an agent ID.
65    pub assignee: String,
66
67    // --- Result ---
68    /// Task output (populated on completion).
69    pub result: Option<serde_json::Value>,
70    /// Error message (populated on failure).
71    pub error: Option<String>,
72
73    // --- Metadata ---
74    /// Extensible key-value metadata. Users add app-specific fields here
75    /// (calendar dates, kanban columns, time tracking, etc.).
76    pub metadata: HashMap<String, serde_json::Value>,
77
78    // --- Lifecycle timestamps ---
79    pub created_at: DateTime<Utc>,
80    pub updated_at: Option<DateTime<Utc>>,
81    pub completed_at: Option<DateTime<Utc>>,
82    /// Soft delete timestamp. None = active. Some = in trash.
83    pub deleted_at: Option<DateTime<Utc>>,
84}
85
86impl Task {
87    /// Create a new task with a title. Defaults: pending, medium priority, human-created.
88    #[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    /// Create an agent-owned task.
113    #[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    /// Create a plan root task (container for subtasks).
126    #[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    /// Whether this task has been soft-deleted.
137    #[must_use]
138    pub fn is_deleted(&self) -> bool {
139        self.deleted_at.is_some()
140    }
141
142    /// Whether this task is in a terminal state (completed or cancelled).
143    #[must_use]
144    pub fn is_terminal(&self) -> bool {
145        matches!(self.status, TaskStatus::Completed | TaskStatus::Cancelled)
146    }
147
148    /// Builder: set description.
149    #[must_use]
150    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
151        self.description = desc.into();
152        self
153    }
154
155    /// Builder: set priority.
156    #[must_use]
157    pub fn with_priority(mut self, priority: TaskPriority) -> Self {
158        self.priority = priority;
159        self
160    }
161
162    /// Builder: set parent task.
163    #[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    /// Builder: add tags.
170    #[must_use]
171    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
172        self.tags = tags;
173        self
174    }
175}
176
177/// Task lifecycle status.
178///
179/// Valid transitions are enforced by [`TaskLifecycle`](crate::lifecycle::TaskLifecycle).
180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
181#[non_exhaustive]
182pub enum TaskStatus {
183    /// Not yet started.
184    Pending,
185    /// Work is actively happening.
186    InProgress,
187    /// Successfully finished.
188    Completed,
189    /// Failed with an error.
190    Failed,
191    /// Waiting on a dependency.
192    Blocked,
193    /// Explicitly abandoned.
194    Cancelled,
195}
196
197/// What created this task.
198#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
199#[non_exhaustive]
200pub enum TaskType {
201    /// Created by framework internals.
202    System,
203    /// Created by an agent during execution.
204    Agent,
205    /// Created by a human user.
206    Human,
207    /// Root of a multi-step plan (container for subtasks).
208    Plan,
209}
210
211/// Priority level (lower number = more urgent).
212#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
213#[non_exhaustive]
214#[derive(Default)]
215pub enum TaskPriority {
216    /// Critical, do immediately.
217    Urgent = 1,
218    /// Important, do soon.
219    High = 2,
220    /// Normal priority.
221    #[default]
222    Medium = 3,
223    /// Do when convenient.
224    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()); // failed is NOT terminal — can retry
297    }
298}