Skip to main content

agent_code_lib/services/
background.rs

1//! Background task execution.
2//!
3//! Manages tasks that run asynchronously while the user continues
4//! interacting with the agent. Tasks output to files and notify
5//! the user when complete.
6
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use tokio::sync::Mutex;
12use tracing::{debug, info};
13
14/// Unique task identifier.
15pub type TaskId = String;
16
17/// Status of a background task.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum TaskStatus {
20    Running,
21    Completed,
22    Failed(String),
23    Killed,
24}
25
26/// Metadata for a running or completed background task.
27#[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
37/// Manages background task lifecycle.
38pub 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    /// Spawn a background shell command.
52    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        // Ensure output directory exists.
62        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        // Spawn the process.
78        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    /// Get the status of a task.
135    pub async fn get_status(&self, id: &str) -> Option<TaskInfo> {
136        self.tasks.lock().await.get(id).cloned()
137    }
138
139    /// Read the output of a completed task.
140    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    /// List all tasks.
150    pub async fn list(&self) -> Vec<TaskInfo> {
151        self.tasks.lock().await.values().cloned().collect()
152    }
153
154    /// Kill a running task (best-effort).
155    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    /// Collect notifications for newly completed tasks.
168    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
185/// Path where task output is stored.
186fn 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}