feat(agent): robustesse routage ask — fix registre session + concurrence (R0+A0) — §14.3/§15
Stabilise le routage de `ask` avant le bind transport MCP (v5). Invariant « 1 agent = 1 employé » durci ; un agent traite un tour à la fois. - R0a garde LaunchAgent : lève AgentAlreadyRunning pour un lancement neuf ciblant un agent déjà vivant sur un autre node (PTY + structuré) ; rebind seulement même-node ou réattache explicite (conversation_id). Idem spawn_agent. - R0b list_live_agents agrège PTY + structuré (LiveSessions) + dédup. - R0c réconciliation des layouts.json à doublons à l'ouverture (host déterministe, idempotent) — corrige « une cellule reset au retour d'onglet ». - R0d UI : option agent désactivée si vivant ailleurs + « aller à la cellule », mapping AGENT_ALREADY_RUNNING. - A0 sérialisation FIFO des tours par agent_id dans ask_agent (verrou tokio par agent ; agents différents en parallèle ; timeout tour 300s, cap attente 600s). Cadrage : .ideai/briefs/orchestration-v5-transport-bind-cadrage.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -15,11 +15,12 @@ use application::{
|
||||
DeleteLayoutInput, DeleteMemoryInput, DeleteSkillInput, DeleteTemplateInput,
|
||||
DetectAgentDriftInput, GetMemoryInput, GitBranchesInput, GitCheckoutInput, GitCommitInput,
|
||||
GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
|
||||
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput,
|
||||
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput, LiveSessions,
|
||||
ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LoadLayoutInput, MutateLayoutInput,
|
||||
OpenProjectInput,
|
||||
ReadAgentContextInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
|
||||
RenameLayoutInput, ResolveMemoryLinksInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
|
||||
ReconcileLayoutsInput, RenameLayoutInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
|
||||
SnapshotRunningAgentsInput,
|
||||
SyncAgentWithTemplateInput, UnassignSkillFromAgentInput, UpdateAgentContextInput,
|
||||
UpdateMemoryInput, UpdateProjectContextInput, UpdateSkillInput,
|
||||
};
|
||||
@ -108,6 +109,15 @@ pub async fn open_project(
|
||||
.execute(OpenProjectInput { project_id: id })
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
// R0c (§3.4 « Trou C ») : dé-doublonne les `layouts.json` portant plusieurs
|
||||
// feuilles sur le même agent AVANT toute reprise (`list_resumable_agents`
|
||||
// relit la version persistée), de sorte qu'on ne propose / ne relance qu'une
|
||||
// session par agent. Idempotent (no-op sans doublon) et best-effort : un échec
|
||||
// ne doit pas bloquer l'ouverture.
|
||||
let _ = state
|
||||
.reconcile_layouts
|
||||
.execute(ReconcileLayoutsInput { project_id: id })
|
||||
.await;
|
||||
// (Re)start the orchestrator watcher for this project (idempotent, §14.3).
|
||||
state.ensure_orchestrator_watch(&output.project);
|
||||
Ok(ProjectDto::from(output))
|
||||
@ -835,9 +845,18 @@ pub async fn list_agents(
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `list_live_agents` — list the agents that currently own a live PTY session
|
||||
/// and the cell hosting each, so the UI can disable an agent already running in
|
||||
/// another cell (the "one live session per agent" invariant).
|
||||
/// `list_live_agents` — list every agent that currently owns a live session
|
||||
/// (raw PTY **or** structured/chat) and the cell hosting each, so the UI can
|
||||
/// disable an agent already running in another cell (the "one live session per
|
||||
/// agent" invariant).
|
||||
///
|
||||
/// Reads the **aggregator** [`LiveSessions::live_agents`], the single source of
|
||||
/// truth for liveness across both registries (PTY + structured). Reading only
|
||||
/// `terminal_sessions` here was blind to structured chat agents, so a live chat
|
||||
/// agent would not be disabled in the UI and could be relaunched elsewhere
|
||||
/// (cadrage v5 §3.3, Trou B). `LiveSessions` is built on the fly from the two
|
||||
/// `Arc` registries already held by [`AppState`]; the aggregation logic itself
|
||||
/// is not duplicated.
|
||||
///
|
||||
/// `project_id` is accepted for API symmetry and future per-project scoping; the
|
||||
/// session registry is process-wide today, so the full live set is returned (a
|
||||
@ -854,9 +873,11 @@ pub fn list_live_agents(
|
||||
// Validate the id shape for a consistent contract, even though the registry
|
||||
// is not project-scoped yet.
|
||||
let _ = parse_project_id(&project_id)?;
|
||||
Ok(LiveAgentListDto::from_pairs(
|
||||
state.terminal_sessions.live_agents(),
|
||||
))
|
||||
let live = LiveSessions::new(
|
||||
std::sync::Arc::clone(&state.terminal_sessions),
|
||||
std::sync::Arc::clone(&state.structured_sessions),
|
||||
);
|
||||
Ok(LiveAgentListDto::from_pairs(live.live_agents()))
|
||||
}
|
||||
|
||||
/// `attach_live_agent` — rebind an already-running agent session to a visible
|
||||
|
||||
@ -1349,11 +1349,19 @@ pub struct LiveAgentListDto(pub Vec<LiveAgentDto>);
|
||||
|
||||
impl LiveAgentListDto {
|
||||
/// Builds the wire list from the registry's `(AgentId, NodeId, SessionId)` tuples.
|
||||
///
|
||||
/// De-duplicates by `agent_id`: the "one live session per agent" invariant
|
||||
/// guarantees an agent is live in at most one registry (PTY **or**
|
||||
/// structured), but the aggregated input could in theory carry the same agent
|
||||
/// twice; the UI contract is a dup-free set (each agent appears once), so we
|
||||
/// keep the first occurrence and drop any later one for the same agent.
|
||||
#[must_use]
|
||||
pub fn from_pairs(pairs: Vec<(AgentId, NodeId, domain::SessionId)>) -> Self {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
Self(
|
||||
pairs
|
||||
.into_iter()
|
||||
.filter(|(agent_id, _, _)| seen.insert(*agent_id))
|
||||
.map(|(agent_id, node_id, session_id)| LiveAgentDto {
|
||||
agent_id: agent_id.to_string(),
|
||||
node_id: node_id.to_string(),
|
||||
|
||||
@ -22,7 +22,8 @@ use application::{
|
||||
ListTemplates,
|
||||
LiveAgentRegistry, LoadLayout, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
|
||||
OpenTerminal, OrchestratorService, ReadAgentContext, ReadMemoryIndex, ReadProjectContext,
|
||||
RecallMemory, ReferenceProfiles, RenameLayout, ResizeTerminal, ResolveMemoryLinks,
|
||||
RecallMemory, ReconcileLayouts, ReferenceProfiles, RenameLayout, ResizeTerminal,
|
||||
ResolveMemoryLinks,
|
||||
SaveEmbedderProfile, SaveProfile, SetActiveLayout, SnapshotRunningAgents, StructuredSessions,
|
||||
SuggestedThisSession,
|
||||
SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent, UpdateAgentContext,
|
||||
@ -98,6 +99,9 @@ pub struct AppState {
|
||||
pub set_active_layout: Arc<SetActiveLayout>,
|
||||
/// Freeze `agent_was_running` on every agent leaf before a PTY kill (T5).
|
||||
pub snapshot_running_agents: Arc<SnapshotRunningAgents>,
|
||||
/// Dé-doublonne, à l'ouverture, les feuilles d'agent en double d'un même
|
||||
/// agent dans `layouts.json` (R0c). Idempotent : no-op sans doublon.
|
||||
pub reconcile_layouts: Arc<ReconcileLayouts>,
|
||||
/// Detect which candidate profiles' CLIs are installed (first-run).
|
||||
pub detect_profiles: Arc<DetectProfiles>,
|
||||
/// List configured profiles.
|
||||
@ -380,6 +384,14 @@ impl AppState {
|
||||
Arc::clone(&terminal_sessions) as Arc<dyn LiveAgentRegistry>,
|
||||
));
|
||||
|
||||
// Twin of the snapshot above, but at *open* time: dé-doublonne les
|
||||
// `layouts.json` portant plusieurs feuilles sur le même agent (R0c, §3.4
|
||||
// « Trou C »). Idempotent : aucun doublon ⇒ aucune écriture.
|
||||
let reconcile_layouts = Arc::new(ReconcileLayouts::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::clone(&fs_port),
|
||||
));
|
||||
|
||||
// --- Profiles & AI runtime (L5) ---
|
||||
// One generic, profile-driven runtime adapter (Open/Closed): it holds the
|
||||
// process spawner used for detection. The profile store persists
|
||||
@ -761,6 +773,7 @@ impl AppState {
|
||||
delete_layout,
|
||||
set_active_layout,
|
||||
snapshot_running_agents,
|
||||
reconcile_layouts,
|
||||
detect_profiles,
|
||||
list_profiles,
|
||||
save_profile,
|
||||
|
||||
Reference in New Issue
Block a user