use crate::models::agent::Agent; use crate::models::agent_task::AgentTask; use crate::models::module::{ProjectModule, MODULE_AGENT_TASK_RUNNER}; use crate::models::project::Project; use crate::services::agent_runtime; use crate::services::process_registry::ProcessRegistry; use rusqlite::Connection; use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter}; use tokio::time::{interval, Duration}; fn build_task_prompt(task: &AgentTask, project: &Project) -> String { format!( r#"Tu es un agent d'exécution de tâches pour un projet logiciel. ## Projet - Nom: {project_name} - Chemin repo: {project_path} - Branche de base: {base_branch} ## Tâche - Titre: {task_title} - Description: {task_description} ## Attendu 1. Exécute la tâche dans le contexte du dépôt. 2. Fais des changements concrets si nécessaire. 3. Donne un rapport final en markdown: - Résumé - Changements - Vérifications faites - Risques / suites "#, project_name = project.name, project_path = project.path, base_branch = project.base_branch, task_title = task.title, task_description = task.description, ) } fn emit_status( app_handle: &AppHandle, project_id: &str, task_id: &str, status: &str, error: Option<&str>, ) { let _ = app_handle.emit( "agent-task-updated", serde_json::json!({ "project_id": project_id, "task_id": task_id, "status": status, "error": error, }), ); } async fn process_next_task( db: &Arc>, app_handle: &AppHandle, process_registry: &ProcessRegistry, ) -> Result { let next_task = { let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; let pending = AgentTask::list_pending(&conn) .map_err(|e| format!("list pending tasks failed: {}", e))?; let mut selected = None; for task in pending { let enabled = ProjectModule::is_enabled(&conn, &task.project_id, MODULE_AGENT_TASK_RUNNER) .map_err(|e| format!("task module lookup failed: {}", e))?; if !enabled { continue; } let agent = Agent::get_by_id(&conn, &task.agent_id) .map_err(|e| format!("agent lookup failed: {}", e))?; if agent.is_runtime_exhausted() { if agent.exhaustion_has_expired() { Agent::mark_available(&conn, &agent.id) .map_err(|e| format!("agent availability reset failed: {}", e))?; } else { continue; } } selected = Some((task, agent)); break; } selected }; let (task, agent) = match next_task { Some(payload) => payload, None => return Ok(false), }; let project = { let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; AgentTask::mark_running(&conn, &task.id) .map_err(|e| format!("mark task running failed: {}", e))?; let project = Project::get_by_id(&conn, &task.project_id) .map_err(|e| format!("project lookup failed: {}", e))?; project }; emit_status(app_handle, &task.project_id, &task.id, "running", None); let prompt = build_task_prompt(&task, &project); let args = agent.tool.to_non_interactive_args(); let result = agent_runtime::run_agent_command_for_task( agent.tool.to_command(), &args, &prompt, &project.path, 900, process_registry, &task.id, ) .await; match result { Ok(report) => { let current = { let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; AgentTask::get_by_id(&conn, &task.id) .map_err(|e| format!("task lookup failed: {}", e))? }; if current.status == "cancelled" { emit_status(app_handle, &task.project_id, &task.id, "cancelled", None); return Ok(true); } let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; AgentTask::mark_done(&conn, &task.id, &report) .map_err(|e| format!("mark task done failed: {}", e))?; emit_status(app_handle, &task.project_id, &task.id, "done", None); } Err(error) => { let current = { let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; AgentTask::get_by_id(&conn, &task.id) .map_err(|e| format!("task lookup failed: {}", e))? }; if current.status == "cancelled" { emit_status(app_handle, &task.project_id, &task.id, "cancelled", None); return Ok(true); } if let Some(exhaustion) = agent_runtime::detect_agent_exhaustion(&error) { let exhausted_until = exhaustion.exhausted_until_rfc3339(); let waiting_note = format!( "Agent '{}' est épuisé (quota/token). Tâche remise en attente jusqu'à {}.", agent.name, exhausted_until ); let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; Agent::mark_exhausted(&conn, &agent.id, &exhaustion.reason, Some(&exhausted_until)) .map_err(|e| format!("mark agent exhausted failed: {}", e))?; AgentTask::mark_pending(&conn, &task.id, Some(&waiting_note)) .map_err(|e| format!("requeue task failed: {}", e))?; emit_status( app_handle, &task.project_id, &task.id, "pending", Some(&waiting_note), ); return Ok(true); } let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; AgentTask::mark_error(&conn, &task.id, &error) .map_err(|e| format!("mark task error failed: {}", e))?; emit_status( app_handle, &task.project_id, &task.id, "error", Some(&error), ); } } Ok(true) } pub fn start(db: Arc>, app_handle: AppHandle, process_registry: ProcessRegistry) { tauri::async_runtime::spawn(async move { let mut tick = interval(Duration::from_secs(8)); loop { tick.tick().await; match process_next_task(&db, &app_handle, &process_registry).await { Ok(true) => continue, Ok(false) => {} Err(e) => { eprintln!("task_runner: {}", e); } } } }); }