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:
@ -107,6 +107,16 @@ impl Agent {
|
||||
})
|
||||
}
|
||||
|
||||
/// Change le profil runtime de l'agent. Le contexte `.md`, l'origine template
|
||||
/// et la synchronisation sont **inchangés** (décision verrouillée : on garde le
|
||||
/// `.md`/mémoire, on ne touche qu'au moteur). Pur, infaillible (un `ProfileId`
|
||||
/// est déjà un VO validé).
|
||||
#[must_use]
|
||||
pub fn with_profile(mut self, profile_id: ProfileId) -> Self {
|
||||
self.profile_id = profile_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a copy of this agent carrying the given assigned skills,
|
||||
/// deduplicated by `skill_id` (keeping first occurrence).
|
||||
#[must_use]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//! Domain events published on the [`crate::ports::EventBus`] and relayed to the
|
||||
//! presentation layer (ARCHITECTURE §3.2).
|
||||
|
||||
use crate::ids::{AgentId, ProjectId, SessionId, SkillId, TemplateId};
|
||||
use crate::ids::{AgentId, ProfileId, ProjectId, SessionId, SkillId, TemplateId};
|
||||
use crate::memory::MemorySlug;
|
||||
use crate::template::TemplateVersion;
|
||||
|
||||
@ -32,6 +32,13 @@ pub enum DomainEvent {
|
||||
/// Exit code.
|
||||
code: i32,
|
||||
},
|
||||
/// An agent's runtime profile was changed (hot-swap of the AI engine).
|
||||
AgentProfileChanged {
|
||||
/// The agent.
|
||||
agent_id: AgentId,
|
||||
/// The new runtime profile it now uses.
|
||||
profile_id: ProfileId,
|
||||
},
|
||||
/// A template was updated (content changed, version bumped).
|
||||
TemplateUpdated {
|
||||
/// The template.
|
||||
|
||||
@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user