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:
2026-09-11 16:42:29 +02:00
parent 1dc892d80b
commit 1316e87010
20 changed files with 1048 additions and 154 deletions

View File

@ -302,6 +302,8 @@ pub enum OrchestratorCommand {
context: Option<String>,
/// Desired visibility for the launched session.
visibility: OrchestratorVisibility,
/// Authenticated caller when the launch originates from an agent tool.
requester: Option<AgentId>,
},
/// Stop/cancel a running agent.
StopAgent {
@ -587,6 +589,7 @@ impl OrchestratorRequest {
profile: Some(self.require("profile", action, self.profile.as_deref())?),
context: self.context.as_ref().filter(|c| !c.is_empty()).cloned(),
visibility: self.parse_visibility(action)?,
requester: self.optional_requester(action)?,
}),
"agent.run" => Ok(OrchestratorCommand::SpawnAgent {
name: self.require_target_agent(action)?,
@ -602,6 +605,7 @@ impl OrchestratorRequest {
.filter(|c| !c.is_empty())
.cloned(),
visibility: self.parse_visibility(action)?,
requester: self.optional_requester(action)?,
}),
"stop_agent" => Ok(OrchestratorCommand::StopAgent {
name: self.require_name(action)?,
@ -981,6 +985,7 @@ mod tests {
profile: Some("claude-code".to_owned()),
context: Some("agents/dev-backend.md".to_owned()),
visibility: OrchestratorVisibility::Background,
requester: None,
}
);
}
@ -995,6 +1000,7 @@ mod tests {
profile: Some("claude-code".to_owned()),
context: None,
visibility: OrchestratorVisibility::Background,
requester: None,
}
);
}
@ -1014,6 +1020,7 @@ mod tests {
visibility: OrchestratorVisibility::Visible {
node_id: NodeId::from_uuid(node)
},
requester: None,
}
);
@ -1053,6 +1060,7 @@ mod tests {
profile: None,
context: Some("Analyse".to_owned()),
visibility: OrchestratorVisibility::Background,
requester: None,
}
);
}
@ -1072,6 +1080,7 @@ mod tests {
visibility: OrchestratorVisibility::Visible {
node_id: NodeId::from_uuid(node)
},
requester: None,
}
);
}