feat(agent): conversation par paire + entrée médiée + pivot terminal/MCP

Coeur inter-agents consolidé et surface front réalignée sur la décision
"terminal natif PTY, pas d'UI chat" (Option 1).

Domaine
- nouveaux modules conversation, mailbox, input, fileguard (ports + types)
- orchestrator/profile/events étendus (conversation par paire, FIFO)

Application / Infrastructure
- orchestrator/service + context_guard : sérialisation FIFO par agent,
  garde RW mémoire/contexte, dispatch ask/reply
- adapters in-memory conversation / mailbox / input / fileguard
- registry session + lifecycle agent durcis (1 agent = 1 session vivante)
- outils MCP idea_* alignés sur le nouveau dispatch

Frontend
- MediatedInput + useAgentBusy : entrée utilisateur médiée par IdeA,
  terminal = vue sortie inchangée
- suppression de la vue chat structurée (AgentChatView) — abandonnée
- adapter input + ports mis à jour

Divers
- .ideai/ : mémoire projet + briefs de cadrage versionnés ;
  requests/ runtime ignoré ; agents projet réels (DevBackend/DevFrontend/QA)

Tests : Rust (domain/application/infrastructure/app-tauri) + front (346) verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 07:33:04 +02:00
parent 5f45c22941
commit eca2ba95c4
61 changed files with 9491 additions and 2512 deletions

View File

@ -9,6 +9,7 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use domain::conversation::ConversationId;
use domain::ports::{AgentSession, PtyHandle};
use domain::{AgentId, NodeId, SessionId, SessionKind, TerminalSession};
@ -44,6 +45,11 @@ pub trait LiveAgentRegistry: Send + Sync {
#[derive(Default)]
pub struct TerminalSessions {
entries: Mutex<HashMap<SessionId, Entry>>,
/// Conversation → live session binding (cadrage C3 §5.2): «1 session vivante /
/// **conversation**» (remplace «1 / agent»). Populated by the orchestrator when an
/// ask resolves/launches the session for a given thread. Separate from `entries`
/// (the domain [`TerminalSession`] does not carry a conversation id).
conversations: Mutex<HashMap<ConversationId, SessionId>>,
}
impl LiveAgentRegistry for TerminalSessions {
@ -65,9 +71,50 @@ impl TerminalSessions {
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
conversations: Mutex::new(HashMap::new()),
}
}
/// Binds `conversation` to the live `session` (cadrage C3 §5.2). Idempotent:
/// re-binding the same conversation overwrites the target session.
pub fn bind_conversation(&self, conversation: ConversationId, session: SessionId) {
if let Ok(mut m) = self.conversations.lock() {
m.insert(conversation, session);
}
}
/// Returns the live [`SessionId`] bound to `conversation`, if any **and** still
/// registered (a stale binding to a closed session resolves to `None`).
///
/// «1 session vivante / conversation» — deterministic, replacing the ambiguous
/// per-agent lookup for the orchestrator's ask path.
#[must_use]
pub fn session_for(&self, conversation: ConversationId) -> Option<SessionId> {
let sid = self.conversations.lock().ok()?.get(&conversation).copied()?;
// Only return it if the session is still live in `entries`.
self.entries
.lock()
.ok()
.filter(|m| m.contains_key(&sid))
.map(|_| sid)
}
/// Lists every live [`SessionId`] hosting `agent_id` (cadrage C3 §1.2 — an agent
/// may take part in several threads, so this is the **plural** of
/// [`Self::session_for_agent`]).
#[must_use]
pub fn sessions_for_agent(&self, agent_id: &AgentId) -> Vec<SessionId> {
self.entries
.lock()
.map(|m| {
m.values()
.filter(|e| matches!(e.session.kind, SessionKind::Agent { agent_id: a } if &a == agent_id))
.map(|e| e.session.id)
.collect()
})
.unwrap_or_default()
}
/// Inserts a freshly-opened session.
pub fn insert(&self, handle: PtyHandle, session: TerminalSession) {
if let Ok(mut map) = self.entries.lock() {
@ -184,8 +231,12 @@ impl TerminalSessions {
.unwrap_or_default()
}
/// Removes a session from the registry, returning its handle if present.
/// Removes a session from the registry, returning its handle if present. Also
/// drops any conversation binding pointing at it (no stale `session_for`).
pub fn remove(&self, id: &SessionId) -> Option<PtyHandle> {
if let Ok(mut c) = self.conversations.lock() {
c.retain(|_, sid| sid != id);
}
self.entries
.lock()
.ok()