j_agent/tools/task/
task_manager.rs1use super::entity::AgentTask;
2use crate::permission::JcliConfig;
3use crate::storage::{load_tasks_state, sessions_dir};
4use crate::util::safe_lock;
5use serde_json::Value;
6use std::fs;
7use std::path::PathBuf;
8use std::sync::Mutex;
9
10#[derive(Debug)]
14pub struct TaskManager {
15 tasks_dir: PathBuf,
16 write_lock: Mutex<()>,
17}
18
19impl Default for TaskManager {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl TaskManager {
26 pub fn new() -> Self {
27 let config_dir = JcliConfig::find_config_dir().or_else(JcliConfig::ensure_config_dir);
29 let tasks_dir = match config_dir {
30 Some(dir) => dir.join("tasks"),
31 None => {
32 let data_dir = crate::constants::data_root();
34 data_dir.join("agent").join("data").join("tasks")
35 }
36 };
37 let _ = fs::create_dir_all(&tasks_dir);
38 Self {
39 tasks_dir,
40 write_lock: Mutex::new(()),
41 }
42 }
43
44 pub fn new_with_session(session_id: &str) -> Self {
46 let sdir = sessions_dir();
47 let session_dir = sdir.join(session_id);
48 let tasks_dir = session_dir.join("tasks_session");
49 let _ = fs::create_dir_all(&tasks_dir);
50 let mgr = Self {
51 tasks_dir,
52 write_lock: Mutex::new(()),
53 };
54 if let Some(tasks) = load_tasks_state(session_id) {
56 mgr.replace_all(tasks);
57 }
58 mgr
59 }
60
61 fn next_id(&self) -> u64 {
63 let mut max_id: u64 = 0;
64 if let Ok(entries) = fs::read_dir(&self.tasks_dir) {
65 for entry in entries.flatten() {
66 let name = entry.file_name();
67 let name = name.to_string_lossy();
68 if let Some(rest) = name.strip_prefix("task_")
70 && let Some(id_str) = rest.strip_suffix(".json")
71 && let Ok(id) = id_str.parse::<u64>()
72 {
73 max_id = max_id.max(id);
74 }
75 }
76 }
77 max_id + 1
78 }
79
80 fn task_path(&self, id: u64) -> PathBuf {
81 self.tasks_dir.join(format!("task_{}.json", id))
82 }
83
84 pub fn create_task(
85 &self,
86 title: &str,
87 description: &str,
88 blocked_by: Vec<u64>,
89 task_doc_paths: Vec<String>,
90 ) -> Result<AgentTask, String> {
91 let _lock = safe_lock(&self.write_lock, "TaskManager::create_task");
92 let task_id = self.next_id();
93 let task = AgentTask {
94 task_id,
95 title: title.to_string(),
96 description: description.to_string(),
97 status: "pending".to_string(),
98 blocked_by,
99 owner: String::new(),
100 task_doc_paths,
101 };
102 self.save_task(&task)?;
103
104 Ok(task)
105 }
106
107 pub fn list_ready_tasks(&self) -> Vec<AgentTask> {
109 self.list_tasks()
110 .into_iter()
111 .filter(|t| t.status == "pending" && t.blocked_by.is_empty())
112 .collect()
113 }
114
115 pub fn get_task(&self, id: u64) -> Result<AgentTask, String> {
116 let path = self.task_path(id);
117 if !path.exists() {
118 return Err(format!("Task {} does not exist", id));
119 }
120 let data = fs::read_to_string(&path).map_err(|e| format!("Failed to read task: {}", e))?;
121 serde_json::from_str(&data).map_err(|e| format!("Failed to parse task: {}", e))
122 }
123
124 pub fn list_tasks(&self) -> Vec<AgentTask> {
125 let mut tasks = Vec::new();
126 if let Ok(entries) = fs::read_dir(&self.tasks_dir) {
127 for entry in entries.flatten() {
128 let path = entry.path();
129 if path.extension().is_some_and(|e| e == "json")
130 && let Ok(data) = fs::read_to_string(&path)
131 && let Ok(task) = serde_json::from_str::<AgentTask>(&data)
132 {
133 tasks.push(task);
134 }
135 }
136 }
137 tasks.sort_by_key(|t| t.task_id);
138 tasks
139 }
140
141 pub fn update_task(&self, id: u64, updates: &Value) -> Result<AgentTask, String> {
142 let _lock = safe_lock(&self.write_lock, "TaskManager::update_task");
143 let mut task = self.get_task(id)?;
144
145 if let Some(status) = updates.get("status").and_then(|s| s.as_str()) {
146 match status {
147 "deleted" => {
148 let path = self.task_path(id);
150 let _ = fs::remove_file(&path);
151 self.clean_references(id);
153 task.status = "deleted".to_string();
154 return Ok(task);
155 }
156 "pending" | "in_progress" | "completed" => {
157 task.status = status.to_string();
158 if status == "completed" {
160 self.clean_references(id);
161 }
162 }
163 _ => return Err(format!("Invalid status: {}", status)),
164 }
165 }
166
167 if let Some(title) = updates.get("title").and_then(|s| s.as_str()) {
168 task.title = title.to_string();
169 }
170 if let Some(description) = updates.get("description").and_then(|s| s.as_str()) {
171 task.description = description.to_string();
172 }
173 if let Some(owner) = updates.get("owner").and_then(|s| s.as_str()) {
174 task.owner = owner.to_string();
175 }
176
177 if let Some(add_blocked_by) = updates.get("addBlockedBy").and_then(|v| v.as_array()) {
179 for id_val in add_blocked_by {
180 if let Some(dep_id) = id_val.as_u64()
181 && !task.blocked_by.contains(&dep_id)
182 {
183 task.blocked_by.push(dep_id);
184 }
185 }
186 }
187
188 self.save_task(&task)?;
189 Ok(task)
190 }
191
192 fn save_task(&self, task: &AgentTask) -> Result<(), String> {
193 let path = self.task_path(task.task_id);
194 let data = serde_json::to_string_pretty(task)
195 .map_err(|e| format!("Failed to serialize task: {}", e))?;
196 fs::write(&path, data).map_err(|e| format!("Failed to write task: {}", e))
197 }
198
199 fn clean_references(&self, completed_id: u64) {
201 let tasks = self.list_tasks();
202 for mut task in tasks {
203 if task.blocked_by.contains(&completed_id) {
204 task.blocked_by.retain(|&id| id != completed_id);
205 let _ = self.save_task(&task);
206 }
207 }
208 }
209
210 pub fn replace_all(&self, new_tasks: Vec<AgentTask>) {
212 let _lock = safe_lock(&self.write_lock, "TaskManager::replace_all");
213 if let Ok(entries) = fs::read_dir(&self.tasks_dir) {
215 for entry in entries.flatten() {
216 let path = entry.path();
217 if path.extension().is_some_and(|e| e == "json") {
218 let _ = fs::remove_file(&path);
219 }
220 }
221 }
222 for task in new_tasks {
224 let _ = self.save_task(&task);
225 }
226 }
227}