feat(agent): backend hot-swap profil (A0+A1) + inventaire reprise session (B1) — L15

Cadrage Architecture §15 (figé) : « agent = entité à session persistante ».

- A0 (domaine) : Agent::with_profile, LayoutTree::leaf, event AgentProfileChanged
  (+ DTO miroir DomainEventDto camelCase).
- A1 (application) : use case ChangeAgentProfile — no-op si profil identique,
  mutation manifeste, nettoyage conversation_id/agent_was_running sur layouts
  persistés, swap à chaud (kill PTY + relance même cellule via composition de
  LaunchAgent), event AgentProfileChanged. Décision : repartir à neuf (on garde
  .md + mémoire, on jette l'historique de conversation).
- B1 (application) : use case ListResumableAgents (lecture seule) — inventaire des
  cellules was_running||conversation_id, resume_supported selon profil, best-effort.

Aucun nouveau port/adapter (composition de l'existant). Hexagonal strict.
Tests : domaine 11 + app-tauri dto + ChangeAgentProfile 9 + ListResumableAgents 8,
suite application complète verte (0 régression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 09:55:44 +02:00
parent 62bd5130fb
commit 2433e173a1
15 changed files with 2630 additions and 11 deletions

View File

@ -620,6 +620,24 @@ impl LayoutTree {
out
}
/// Retrouve la [`LeafCell`] portant l'identifiant `node`.
///
/// Renvoie `None` si aucun node ne porte cet id, **ou** si le node existe mais
/// est un split/grid (pas une feuille). Traversée pure en lecture seule, dans
/// le même esprit que [`Self::agent_leaves`] / [`Self::session_in_leaf`].
#[must_use]
pub fn leaf(&self, node: NodeId) -> Option<&LeafCell> {
fn find(n: &LayoutNode, id: NodeId) -> Option<&LeafCell> {
match n {
LayoutNode::Leaf(leaf) if leaf.id == id => Some(leaf),
LayoutNode::Leaf(_) => None,
LayoutNode::Split(split) => split.children.iter().find_map(|c| find(&c.node, id)),
LayoutNode::Grid(grid) => grid.cells.iter().find_map(|c| find(&c.node, id)),
}
}
find(&self.root, node)
}
/// Returns `Ok(Some(session))` / `Ok(None)` for the session held by the leaf
/// `id`, or [`LayoutError::NodeNotFound`] if no such leaf exists.
fn session_in_leaf(&self, id: NodeId) -> Result<Option<SessionId>, LayoutError> {