orchai/src-tauri/src/services/task_runner.rs

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);
}
}
}
});
}