1use crate::config::RuntimeConfig;
23use crate::error::{RavenClawsError, Result};
24use crate::healing::SelfHealingEngine;
25use crate::llm::LLMProviderTrait;
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28use std::path::{Path, PathBuf};
29use std::sync::Arc;
30use tokio::sync::RwLock;
31use tracing::{info, instrument, warn};
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub enum TaskStatus {
36 Pending,
38 Running,
40 Completed,
42 Failed,
44 Cancelled,
46}
47
48impl std::fmt::Display for TaskStatus {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 TaskStatus::Pending => write!(f, "pending"),
52 TaskStatus::Running => write!(f, "running"),
53 TaskStatus::Completed => write!(f, "completed"),
54 TaskStatus::Failed => write!(f, "failed"),
55 TaskStatus::Cancelled => write!(f, "cancelled"),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct BackgroundTask {
63 pub id: String,
65 pub prompt: String,
67 pub system_prompt: String,
69 pub status: TaskStatus,
71 pub result: Option<String>,
73 pub error: Option<String>,
75 pub created_at: String,
77 pub updated_at: String,
79 pub iterations: usize,
81 pub provider: Option<String>,
83 pub model: Option<String>,
85 pub checkpoint: Option<String>,
89}
90
91impl BackgroundTask {
92 pub fn new(id: String, prompt: String, system_prompt: String) -> Self {
94 let now = chrono::Utc::now().to_rfc3339();
95 Self {
96 id,
97 prompt,
98 system_prompt,
99 status: TaskStatus::Pending,
100 result: None,
101 error: None,
102 created_at: now.clone(),
103 updated_at: now,
104 iterations: 0,
105 provider: None,
106 model: None,
107 checkpoint: None,
108 }
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct BackgroundTaskManager {
115 tasks_dir: PathBuf,
117 tasks: Arc<RwLock<HashMap<String, BackgroundTask>>>,
119 healing_engine: Option<Arc<std::sync::Mutex<SelfHealingEngine>>>,
121}
122
123impl BackgroundTaskManager {
124 pub async fn new(tasks_dir: &Path) -> Result<Self> {
130 let tasks_dir = tasks_dir.to_path_buf();
131
132 std::fs::create_dir_all(&tasks_dir).map_err(|e| {
134 RavenClawsError::CommandExecution(format!(
135 "Failed to create tasks directory '{}': {}",
136 tasks_dir.display(),
137 e
138 ))
139 })?;
140
141 let tasks = Arc::new(RwLock::new(HashMap::new()));
142
143 let mut manager = Self {
144 tasks_dir,
145 tasks,
146 healing_engine: None,
147 };
148
149 let count = manager.load_tasks().await?;
151 if count > 0 {
152 info!(count, "Loaded existing background tasks from disk");
153 }
154
155 Ok(manager)
156 }
157
158 pub async fn from_config(config: &RuntimeConfig) -> Result<Self> {
160 let tasks_dir = PathBuf::from(&config.workdir).join("tasks");
161 Self::new(&tasks_dir).await
162 }
163
164 async fn load_tasks(&mut self) -> Result<usize> {
166 let mut count = 0;
167 let read_dir = match std::fs::read_dir(&self.tasks_dir) {
168 Ok(d) => d,
169 Err(_) => return Ok(0),
170 };
171
172 let mut tasks_to_insert = Vec::new();
173 for entry in read_dir.flatten() {
174 let path = entry.path();
175 if path.extension().is_some_and(|ext| ext == "json") {
176 match std::fs::read_to_string(&path) {
177 Ok(content) => match serde_json::from_str::<BackgroundTask>(&content) {
178 Ok(task) => {
179 tasks_to_insert.push(task);
180 count += 1;
181 }
182 Err(e) => {
183 warn!(
184 path = %path.display(),
185 error = %e,
186 "Failed to deserialize background task"
187 );
188 }
189 },
190 Err(e) => {
191 warn!(
192 path = %path.display(),
193 error = %e,
194 "Failed to read background task file"
195 );
196 }
197 }
198 }
199 }
200
201 let mut tasks = self.tasks.write().await;
202 for task in tasks_to_insert {
203 tasks.insert(task.id.clone(), task);
204 }
205
206 Ok(count)
207 }
208
209 fn save_task(&self, task: &BackgroundTask) -> Result<()> {
211 let path = self.tasks_dir.join(format!("{}.json", task.id));
212 let content = serde_json::to_string_pretty(task).map_err(|e| {
213 RavenClawsError::CommandExecution(format!("Failed to serialize task: {}", e))
214 })?;
215
216 std::fs::write(&path, content).map_err(|e| {
217 RavenClawsError::CommandExecution(format!(
218 "Failed to write task file '{}': {}",
219 path.display(),
220 e
221 ))
222 })?;
223
224 Ok(())
225 }
226
227 pub async fn submit(&self, prompt: String, system_prompt: String) -> Result<String> {
230 let id = uuid::Uuid::new_v4().to_string();
231 let task = BackgroundTask::new(id.clone(), prompt, system_prompt);
232
233 self.save_task(&task)?;
235
236 let mut tasks = self.tasks.write().await;
238 tasks.insert(id.clone(), task);
239
240 info!(task_id = %id, "Background task submitted");
241 Ok(id)
242 }
243
244 #[instrument(skip(self, llm), fields(task_id = %task_id))]
247 pub async fn execute(&self, task_id: &str, llm: Arc<dyn LLMProviderTrait>) -> Result<String> {
248 {
250 let mut tasks = self.tasks.write().await;
251 let task = tasks.get_mut(task_id).ok_or_else(|| {
252 RavenClawsError::CommandExecution(format!("Task '{}' not found", task_id))
253 })?;
254
255 task.status = TaskStatus::Running;
256 task.provider = Some(llm.provider_name().to_string());
257 task.model = Some(llm.model().to_string());
258 task.updated_at = chrono::Utc::now().to_rfc3339();
259 self.save_task(task)?;
260 }
261
262 info!(
263 task_id = %task_id,
264 provider = llm.provider_name(),
265 model = llm.model(),
266 "Executing background task"
267 );
268
269 if let Some(ref healing) = self.healing_engine {
271 let healthy = {
272 let mut engine = healing.lock().unwrap();
273 engine.is_healthy(task_id)
274 };
275 if !healthy {
276 let err_msg = format!(
277 "Background task '{}' blocked by self-healing circuit breaker",
278 task_id
279 );
280 warn!(task_id = %task_id, "{}", err_msg);
281
282 let mut tasks = self.tasks.write().await;
284 if let Some(task) = tasks.get_mut(task_id) {
285 task.status = TaskStatus::Failed;
286 task.error = Some(err_msg.clone());
287 task.updated_at = chrono::Utc::now().to_rfc3339();
288 self.save_task(task)?;
289 }
290
291 return Err(RavenClawsError::HealingError(err_msg));
292 }
293 }
294
295 let checkpoint_dir = self.tasks_dir.join("checkpoints");
297 let _ = std::fs::create_dir_all(&checkpoint_dir);
298
299 let loop_config = crate::agent::AgentLoopConfig {
301 max_iterations: 10,
302 enable_tools: true,
303 require_approval: false,
304 prompt_injection_protection: true,
305 token_lifetime_secs: 0,
306 no_final_required: true,
307 fallback_chain: None,
308 token_budget: None,
309 ravenfabric: None,
310 checkpoint_dir: Some(checkpoint_dir),
311 session_id: Some(task_id.to_string()),
312 metrics_callback: None,
313 load_manager: None,
314 retry_config: None,
315 healing_engine: self.healing_engine.clone(),
316 };
317
318 let result = crate::agent::run_agent_loop(
319 llm.clone(),
320 &self.get_prompt(task_id).await?,
321 &self.get_system_prompt(task_id).await?,
322 loop_config,
323 )
324 .await;
325
326 let mut tasks = self.tasks.write().await;
328 let task = tasks.get_mut(task_id).ok_or_else(|| {
329 RavenClawsError::CommandExecution(format!("Task '{}' not found", task_id))
330 })?;
331
332 match result {
333 Ok(response) => {
334 task.status = TaskStatus::Completed;
335 task.result = Some(response.clone());
336 task.updated_at = chrono::Utc::now().to_rfc3339();
337 self.save_task(task)?;
338
339 info!(
340 task_id = %task_id,
341 iterations = task.iterations,
342 "Background task completed"
343 );
344
345 Ok(response)
346 }
347 Err(e) => {
348 task.status = TaskStatus::Failed;
349 task.error = Some(e.to_string());
350 task.updated_at = chrono::Utc::now().to_rfc3339();
351 self.save_task(task)?;
352
353 if let Some(ref healing) = self.healing_engine {
355 let mut engine = healing.lock().unwrap();
356 engine.record_failure(task_id, &e.to_string());
357 }
358
359 warn!(
360 task_id = %task_id,
361 error = %e,
362 "Background task failed"
363 );
364
365 Err(e)
366 }
367 }
368 }
369
370 #[allow(dead_code)]
372 pub async fn status(&self, task_id: &str) -> Result<TaskStatus> {
373 let tasks = self.tasks.read().await;
374 let task = tasks.get(task_id).ok_or_else(|| {
375 RavenClawsError::CommandExecution(format!("Task '{}' not found", task_id))
376 })?;
377 Ok(task.status.clone())
378 }
379
380 pub async fn get_task(&self, task_id: &str) -> Result<BackgroundTask> {
382 let tasks = self.tasks.read().await;
383 tasks.get(task_id).cloned().ok_or_else(|| {
384 RavenClawsError::CommandExecution(format!("Task '{}' not found", task_id))
385 })
386 }
387
388 pub async fn list_tasks(&self) -> Vec<BackgroundTask> {
390 let tasks = self.tasks.read().await;
391 let mut task_list: Vec<BackgroundTask> = tasks.values().cloned().collect();
392 task_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
394 task_list
395 }
396
397 pub async fn cancel(&self, task_id: &str) -> Result<()> {
399 let mut tasks = self.tasks.write().await;
400 let task = tasks.get_mut(task_id).ok_or_else(|| {
401 RavenClawsError::CommandExecution(format!("Task '{}' not found", task_id))
402 })?;
403
404 match task.status {
405 TaskStatus::Pending | TaskStatus::Running => {
406 task.status = TaskStatus::Cancelled;
407 task.updated_at = chrono::Utc::now().to_rfc3339();
408 self.save_task(task)?;
409 info!(task_id = %task_id, "Background task cancelled");
410 Ok(())
411 }
412 _ => Err(RavenClawsError::CommandExecution(format!(
413 "Cannot cancel task '{}' in status '{}'",
414 task_id, task.status
415 ))),
416 }
417 }
418
419 #[allow(dead_code)]
421 pub fn set_healing_engine(&mut self, engine: Option<Arc<std::sync::Mutex<SelfHealingEngine>>>) {
422 self.healing_engine = engine;
423 }
424
425 pub async fn resume_incomplete(&self) -> Vec<String> {
428 let tasks = self.tasks.read().await;
429 let mut incomplete = Vec::new();
430
431 for task in tasks.values() {
432 if task.status == TaskStatus::Pending || task.status == TaskStatus::Running {
433 incomplete.push(task.id.clone());
434 }
435 }
436
437 if !incomplete.is_empty() {
438 info!(
439 count = incomplete.len(),
440 "Found incomplete background tasks to resume"
441 );
442 }
443
444 incomplete
445 }
446
447 async fn get_prompt(&self, task_id: &str) -> Result<String> {
449 let tasks = self.tasks.read().await;
450 let task = tasks.get(task_id).ok_or_else(|| {
451 RavenClawsError::CommandExecution(format!("Task '{}' not found", task_id))
452 })?;
453 Ok(task.prompt.clone())
454 }
455
456 async fn get_system_prompt(&self, task_id: &str) -> Result<String> {
458 let tasks = self.tasks.read().await;
459 let task = tasks.get(task_id).ok_or_else(|| {
460 RavenClawsError::CommandExecution(format!("Task '{}' not found", task_id))
461 })?;
462 Ok(task.system_prompt.clone())
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use std::path::PathBuf;
470
471 fn test_dir(name: &str) -> PathBuf {
472 let dir = std::env::temp_dir().join(format!(
473 "ravenclaws-test-bg-{}-{}",
474 name,
475 std::process::id()
476 ));
477 let _ = std::fs::remove_dir_all(&dir);
478 dir
479 }
480
481 #[tokio::test]
482 async fn test_manager_new_creates_directory() {
483 let dir = test_dir("create_dir");
484 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
485 assert!(dir.exists(), "Tasks directory should be created");
486 assert!(manager.tasks.read().await.is_empty());
487 let _ = std::fs::remove_dir_all(&dir);
488 }
489
490 #[tokio::test]
491 async fn test_submit_task() {
492 let dir = test_dir("submit");
493 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
494
495 let task_id = manager
496 .submit("Test prompt".to_string(), "Test system".to_string())
497 .await
498 .unwrap();
499
500 let task = manager.get_task(&task_id).await.unwrap();
501 assert_eq!(task.prompt, "Test prompt");
502 assert_eq!(task.system_prompt, "Test system");
503 assert_eq!(task.status, TaskStatus::Pending);
504 assert!(task.result.is_none());
505
506 let task_path = dir.join(format!("{}.json", task_id));
508 assert!(task_path.exists(), "Task file should exist on disk");
509
510 let _ = std::fs::remove_dir_all(&dir);
511 }
512
513 #[tokio::test]
514 async fn test_status_transitions() {
515 let dir = test_dir("transitions");
516 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
517
518 let task_id = manager
519 .submit("Test".to_string(), "System".to_string())
520 .await
521 .unwrap();
522
523 assert_eq!(manager.status(&task_id).await.unwrap(), TaskStatus::Pending);
524
525 manager.cancel(&task_id).await.unwrap();
527 assert_eq!(
528 manager.status(&task_id).await.unwrap(),
529 TaskStatus::Cancelled
530 );
531
532 let _ = std::fs::remove_dir_all(&dir);
533 }
534
535 #[tokio::test]
536 async fn test_list_tasks() {
537 let dir = test_dir("list");
538 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
539
540 manager
541 .submit("Task 1".to_string(), "System".to_string())
542 .await
543 .unwrap();
544 manager
545 .submit("Task 2".to_string(), "System".to_string())
546 .await
547 .unwrap();
548
549 let tasks = manager.list_tasks().await;
550 assert_eq!(tasks.len(), 2);
551
552 let _ = std::fs::remove_dir_all(&dir);
553 }
554
555 #[tokio::test]
556 async fn test_cancel_completed_task_fails() {
557 let dir = test_dir("cancel_fail");
558 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
559
560 let task_id = manager
561 .submit("Test".to_string(), "System".to_string())
562 .await
563 .unwrap();
564
565 {
567 let mut tasks = manager.tasks.write().await;
568 let task = tasks.get_mut(&task_id).unwrap();
569 task.status = TaskStatus::Completed;
570 }
571
572 let result = manager.cancel(&task_id).await;
573 assert!(result.is_err(), "Cancelling a completed task should fail");
574
575 let _ = std::fs::remove_dir_all(&dir);
576 }
577
578 #[tokio::test]
579 async fn test_resume_incomplete_tasks() {
580 let dir = test_dir("resume");
581 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
582
583 manager
584 .submit("Task 1".to_string(), "System".to_string())
585 .await
586 .unwrap();
587 manager
588 .submit("Task 2".to_string(), "System".to_string())
589 .await
590 .unwrap();
591
592 {
594 let tasks = manager.tasks.read().await;
595 let tasks_vec: Vec<&BackgroundTask> = tasks.values().collect();
596 if let Some(task) = tasks_vec.first() {
597 let id = task.id.clone();
598 drop(tasks);
599 let mut tasks = manager.tasks.write().await;
600 if let Some(t) = tasks.get_mut(&id) {
601 t.status = TaskStatus::Completed;
602 }
603 }
604 }
605
606 let incomplete = manager.resume_incomplete().await;
607 assert_eq!(incomplete.len(), 1, "One task should be incomplete");
608
609 let _ = std::fs::remove_dir_all(&dir);
610 }
611
612 #[tokio::test]
613 async fn test_task_not_found() {
614 let dir = test_dir("not_found");
615 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
616
617 let result = manager.status("nonexistent").await;
618 assert!(result.is_err());
619
620 let _ = std::fs::remove_dir_all(&dir);
621 }
622
623 #[tokio::test]
624 async fn test_persistence_across_restart() {
625 let dir = test_dir("persist");
626
627 {
629 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
630 manager
631 .submit("Persist test".to_string(), "System".to_string())
632 .await
633 .unwrap();
634 } {
638 let manager = BackgroundTaskManager::new(&dir).await.unwrap();
639 let tasks = manager.list_tasks().await;
640 assert_eq!(tasks.len(), 1, "Task should persist across restarts");
641 assert_eq!(tasks[0].prompt, "Persist test");
642 }
643
644 let _ = std::fs::remove_dir_all(&dir);
645 }
646}