210 lines
6.8 KiB
Rust
210 lines
6.8 KiB
Rust
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<Mutex<Connection>>,
|
|
app_handle: &AppHandle,
|
|
process_registry: &ProcessRegistry,
|
|
) -> Result<bool, String> {
|
|
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<Mutex<Connection>>, 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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|