agent_code_lib/services/
background.rs1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use tokio::sync::Mutex;
12use tracing::{debug, info};
13
14pub type TaskId = String;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum TaskStatus {
20 Running,
21 Completed,
22 Failed(String),
23 Killed,
24}
25
26#[derive(Debug, Clone)]
28pub struct TaskInfo {
29 pub id: TaskId,
30 pub description: String,
31 pub status: TaskStatus,
32 pub output_file: PathBuf,
33 pub started_at: std::time::Instant,
34 pub finished_at: Option<std::time::Instant>,
35}
36
37pub struct TaskManager {
39 tasks: Arc<Mutex<HashMap<TaskId, TaskInfo>>>,
40 next_id: Arc<Mutex<u64>>,
41}
42
43impl TaskManager {
44 pub fn new() -> Self {
45 Self {
46 tasks: Arc::new(Mutex::new(HashMap::new())),
47 next_id: Arc::new(Mutex::new(1)),
48 }
49 }
50
51 pub async fn spawn_shell(
53 &self,
54 command: &str,
55 description: &str,
56 cwd: &Path,
57 ) -> Result<TaskId, String> {
58 let id = self.allocate_id("b").await;
59 let output_file = task_output_path(&id);
60
61 if let Some(parent) = output_file.parent() {
63 let _ = std::fs::create_dir_all(parent);
64 }
65
66 let info = TaskInfo {
67 id: id.clone(),
68 description: description.to_string(),
69 status: TaskStatus::Running,
70 output_file: output_file.clone(),
71 started_at: std::time::Instant::now(),
72 finished_at: None,
73 };
74
75 self.tasks.lock().await.insert(id.clone(), info);
76
77 let task_id = id.clone();
79 let tasks = self.tasks.clone();
80 let command = command.to_string();
81 let cwd = cwd.to_path_buf();
82
83 tokio::spawn(async move {
84 let result = tokio::process::Command::new("bash")
85 .arg("-c")
86 .arg(&command)
87 .current_dir(&cwd)
88 .stdout(std::process::Stdio::piped())
89 .stderr(std::process::Stdio::piped())
90 .output()
91 .await;
92
93 let mut tasks = tasks.lock().await;
94 if let Some(info) = tasks.get_mut(&task_id) {
95 info.finished_at = Some(std::time::Instant::now());
96
97 match result {
98 Ok(output) => {
99 let mut content = String::new();
100 let stdout = String::from_utf8_lossy(&output.stdout);
101 let stderr = String::from_utf8_lossy(&output.stderr);
102 if !stdout.is_empty() {
103 content.push_str(&stdout);
104 }
105 if !stderr.is_empty() {
106 content.push_str("\nstderr:\n");
107 content.push_str(&stderr);
108 }
109 let _ = std::fs::write(&info.output_file, &content);
110
111 if output.status.success() {
112 info.status = TaskStatus::Completed;
113 } else {
114 info.status = TaskStatus::Failed(format!(
115 "Exit code: {}",
116 output.status.code().unwrap_or(-1)
117 ));
118 }
119 }
120 Err(e) => {
121 info.status = TaskStatus::Failed(e.to_string());
122 let _ = std::fs::write(&info.output_file, e.to_string());
123 }
124 }
125
126 info!("Background task {} finished: {:?}", task_id, info.status);
127 }
128 });
129
130 debug!("Background task {id} started: {description}");
131 Ok(id)
132 }
133
134 pub async fn get_status(&self, id: &str) -> Option<TaskInfo> {
136 self.tasks.lock().await.get(id).cloned()
137 }
138
139 pub async fn read_output(&self, id: &str) -> Result<String, String> {
141 let tasks = self.tasks.lock().await;
142 let info = tasks
143 .get(id)
144 .ok_or_else(|| format!("Task '{id}' not found"))?;
145 std::fs::read_to_string(&info.output_file)
146 .map_err(|e| format!("Failed to read output: {e}"))
147 }
148
149 pub async fn list(&self) -> Vec<TaskInfo> {
151 self.tasks.lock().await.values().cloned().collect()
152 }
153
154 pub async fn kill(&self, id: &str) -> Result<(), String> {
156 let mut tasks = self.tasks.lock().await;
157 let info = tasks
158 .get_mut(id)
159 .ok_or_else(|| format!("Task '{id}' not found"))?;
160 if info.status == TaskStatus::Running {
161 info.status = TaskStatus::Killed;
162 info.finished_at = Some(std::time::Instant::now());
163 }
164 Ok(())
165 }
166
167 pub async fn drain_completions(&self) -> Vec<TaskInfo> {
169 let tasks = self.tasks.lock().await;
170 tasks
171 .values()
172 .filter(|t| matches!(t.status, TaskStatus::Completed | TaskStatus::Failed(_)))
173 .cloned()
174 .collect()
175 }
176
177 async fn allocate_id(&self, prefix: &str) -> TaskId {
178 let mut next = self.next_id.lock().await;
179 let id = format!("{prefix}{next}");
180 *next += 1;
181 id
182 }
183}
184
185fn task_output_path(id: &TaskId) -> PathBuf {
187 let dir = dirs::cache_dir()
188 .unwrap_or_else(|| PathBuf::from("/tmp"))
189 .join("agent-code")
190 .join("tasks");
191 dir.join(format!("{id}.out"))
192}