fix(application,domain,infrastructure,web-server,app-tauri,frontend): arrêt d'agent fiable avec cascade sur l'arbre de propriété et statut cohérent — #320 (QA verte)
Symptôme: stopper un agent (ex Main sur UnityPlugin) laissait des travaux poursuivre, la cascade n'atteignait pas les agents lancés et la pastille restait orange pendant que l'agent continuait à en appeler d'autres. - barrière d'admission par génération d'exécution (ExecutionControl): un stop ferme l'admission de tout le sous-arbre propriété/attente AVANT toute annulation asynchrone — plus aucune nouvelle action inter-agents après stop - arbre de propriété durable en-process (launched_by) + dépendances d'attente actives: la cascade couvre transitivement agents et travaux lancés - commandes d'un agent arrêté rejetées (AppError::AgentStopped); stop lui-même admis pour rester idempotent; réactivation sur relance humaine (génération +1) et enregistrement du propriétaire au spawn - admission exposée aux adaptateurs hors commandes cœur (tickets, templates, plugins) via ensure_agent_execution_active; MCP server/tools, web-server et commandes Tauri alignés - cohérence live-state: statut Idle avec intent « Arrêté » ou « Arrêt incomplet — Certains travaux sont encore actifs » selon l'issue de la cascade; workstate et surfaces UI (AgentsPanel, LayoutGrid, ProjectWorkStatePanel) reflètent l'état réel, plus de pastille orange fantôme - tests: orchestrator_service (cascade/générations/idempotence), workstate_actions, background_task_cancel, error_codes, agents, layout, workstate QA verte sur fix/320-agent-stop-cascade. Aucun push.
This commit is contained in:
@ -472,6 +472,21 @@ impl McpServer {
|
||||
self.enforce_durable_tool_policy(&policy, &name)?;
|
||||
}
|
||||
|
||||
// Stop closes the entire MCP surface synchronously, including provider
|
||||
// tools that do not map through OrchestratorCommand. The stop tool itself
|
||||
// stays admitted so repeated/self stops remain idempotent.
|
||||
if name != "idea_stop_agent" {
|
||||
if let Ok(raw) = uuid::Uuid::parse_str(&self.requester) {
|
||||
if let Err(error) = self
|
||||
.service
|
||||
.ensure_agent_execution_active(&self.project, domain::AgentId::from_uuid(raw))
|
||||
{
|
||||
self.publish_processed(&name, false);
|
||||
return Ok(tool_result_text(&error.to_string(), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostics begin beacon (best-effort, jamais le corps task/result) : trace
|
||||
// l'entrée d'un `tools/call`, son tool, le peer demandeur et la cible/longueurs.
|
||||
let started = Instant::now();
|
||||
|
||||
@ -620,6 +620,7 @@ pub fn map_tool_call(
|
||||
},
|
||||
"idea_launch_agent" => OrchestratorRequest {
|
||||
request_type: Some("agent.run".to_owned()),
|
||||
requested_by: Some(requester.to_owned()),
|
||||
target_agent: s("target"),
|
||||
profile: s("profile"),
|
||||
task: s("task"),
|
||||
@ -1210,10 +1211,22 @@ mod tests {
|
||||
profile: None,
|
||||
context: None,
|
||||
visibility: OrchestratorVisibility::Background,
|
||||
requester: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_agent_maps_handshake_requester_for_stop_ownership() {
|
||||
assert!(matches!(
|
||||
map_tool_call("idea_launch_agent", &json!({ "target": "Dev" }), REQ).unwrap(),
|
||||
OrchestratorCommand::SpawnAgent {
|
||||
requester: Some(agent),
|
||||
..
|
||||
} if agent == domain::AgentId::from_uuid(uuid::Uuid::parse_str(REQ).unwrap())
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_update_and_skill_map_to_their_commands() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user