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>
32 KiB
Conversation par paire — cadrage d'architecture (multi-agent solide par construction)
Agent Architecture. Ce document tranche le modèle qui rend le multi-agent solide par construction : l'utilisateur n'a plus à « faire les choses dans le bon ordre ». Aucun code de production ici — décisions, contrats (ports/entités), découpage en lots testables, frontière backend/frontend.
Décisions produit arbitrées (NON négociables, rappel) : (A) conversation par paire = un fil entre deux parties, session propre, matérialisation paresseuse ; (B) entrée médiée par IdeA (le terminal xterm reste la vue de sortie brute INCHANGÉE, seule l'entrée change de chemin), Envoyer=enqueue / Interrompre=préempte ; (C) FileGuard borné aux
.mdde contexte + la mémoire, via outils MCP, verrou lecteurs/écrivain ; (D) zéro git, hexagonal+SOLID stricts, corrélation par ticket, MCP Claude-only, fixbind_endpoint, abandon du band-aid\n→\r.État du terrain (lu, pas présumé). L'essentiel des briques existe déjà :
domain/src/mailbox.rs(AgentMailbox,Ticket,TicketId,PendingReply,MailboxError) ;infrastructure/src/mailbox/mod.rs(InMemoryMailbox, FIFO par agent +oneshot) ;application/src/orchestrator/service.rs(ask_agent,reply,ensure_live_pty, verrou de tourask_locks) ; surface MCP complète (mcp/tools.rs,mcp/server.rs) ; transport bindé (app-tauri/src/mcp_endpoint.rs,state.rs::bind_endpoint/ensure_mcp_server/serve_peer). Ce cadrage formalise et complète ; il ne réécrit pas.
0. Synthèse exécutive (décisions tranchées)
-
La conversation devient une entité de premier plan (
Conversation+ConversationId), absente aujourd'hui. Le couplage actuel « 1 session vivante / agent » (session-registry-agent-ambiguity) est remplacé par « 1 session vivante / conversation ». Un agent peut donc avoir N sessions simultanées (une par fil), mais une seule tâche traitée à la fois (l'entrée reste sérialisée, §B). C'est ce qui supprime la fuite de contexte : la délégation A→B n'emprunte plus la conversation User↔B. -
L'entrée passe par un
InputMediator(nouveau port application) : toutes les entrées (humaine et inter-agents) convergent vers une file FIFO unique par agent,enqueue/preemptdistincts. Le terminal xterm n'écrit plus jamais en direct dans le PTY ; il devient une vue de sortie pure. La file existante (AgentMailbox+ask_locks) est absorbée par leInputMediator: la messagerie inter-agents n'est qu'une source d'entrée parmi deux. -
FileGuard(nouveau port domaine) : un verrou lecteurs/écrivain borné aux fichiers qu'IdeA possède (.mdde contexte d'agent + mémoire). Les agents perdent l'accès fs brut à ces chemins et passent par de nouveaux outils MCPidea_context_read/proposeetidea_memory_read/write. Le contexte global projet est mono-écrivain (l'orchestrateur) ; les autres proposent. -
Détection occupé/libre = double signal avec fallback sûr : (a) retour-de-prompt détecté par motif déclaré dans le profil CLI, (b) signal explicite de l'agent (un
idea_reply, ou fin de tour MCP). En cas de doute → forwarder (on enqueue ; jamais piéger un message). L'occupé/libre remonte au front via unDomainEvent(Channel Tauri), pas via parsing front. -
Fixes durables embarqués :
bind_endpointunlink déjà le socket cadavre (reclaim_name(true), état OK — on verrouille ce comportement par un test de non-régression) ; le band-aid\n→\ret l'« injection PTV » deservice.rs:459disparaissent (l'entrée passe désormais par leInputMediator, pas par une écriture PTY préfixée d'un orchestrateur). -
Garde-fous d'orchestration : timeout par tour (déjà), plafond d'attente en file (déjà,
ASK_QUEUE_WAIT_CAP), détection de cycle sur un graphe wait-for (nouveau, dans le domaine — pur, testable) pour refuser une délégation ré-entrante (A→B→A) avant deadlock.
1. Modèle de domaine
1.1 Nouvelles entités / VO
ConversationId (VO)
newtype(uuid::Uuid), calqué surTicketId/AgentId. Immuable, non vide.- Implémenté :
crates/domain/src/conversation.rs(nouveau module, à exporter danslib.rsà côté demailbox).
ConversationParty (VO, enum)
ConversationParty =
| User // l'humain (une seule instance logique côté IdeA)
| Agent(AgentId) // un agent du projet
- Invariant : une
Conversationrelie deux parties distinctes (jamaisAgent(x)↔Agent(x), jamaisUser↔User).
Conversation (entité)
Conversation {
id: ConversationId,
left: ConversationParty,
right: ConversationParty,
session: ConversationSession, // état d'I/O (voir 1.2)
resumable_id: Option<String>, // session-id reprenable de la CLI (suspend = stocke)
}
- Invariants :
left != right; au plus une des deux parties estUser; identité d'une conversation = la paire non ordonnée{left, right}pour un agent donné (deux paires identiques ⇒ même conversation — clé de la matérialisation paresseuse). Pur, I/O-free. - Matérialisation paresseuse : une
ConversationAgent↔Agentn'existe en registre que s'il y a au moins une tâche ; suspendue, elle ne garde queresumable_id(pas de session vivante). C'est une règle duConversationRegistry(application), pas un champ persistant lourd.
ConversationSession (VO, enum — l'état d'I/O du fil)
ConversationSession =
| Dormant // jamais lancée, ou suspendue (resumable_id seul)
| Live { handle_ref: SessionRef } // un flux d'I/O vivant (PTY ou structuré)
SessionRef= abstraction d'un handle de session (référence vers uneTerminalSessionexistante, cf.domain/src/terminal.rs). Le domaine ne tient pas le PTY (infra).
Task / Ticket (extension de l'existant)
Ticket(domain/src/mailbox.rs) est étendu pour porter l'origine et la conversation cible :
Ticket {
id: TicketId, // existant
source: InputSource, // NOUVEAU : Human | Agent(AgentId)
conversation: ConversationId, // NOUVEAU : le fil dans lequel la tâche entre
requester: String, // existant (label d'affichage du préfixe)
task: String, // existant
}
InputSource(VO, enum) :Human | Agent(AgentId). Remplace l'actuelrequester: Stringlibre comme source de vérité (leStringreste un label d'affichage dérivé). Permet de propager l'identité du demandeur (D) et d'alimenter le graphe wait-for (détection de cycle).- Compat :
Ticket::newgarde sa signature ; on ajouteTicket::from_human(...)etTicket::from_agent(source, conversation, ...)(Open/Closed, pas de breaking).
File FIFO + état occupé/libre (VO)
AgentInbox(concept porté par le portInputMediator, pas une entité persistée) : une file FIFO parAgentId, une tâche en cours à la fois.AgentBusyState(VO, enum) :Idle | Busy { ticket: TicketId, since_ms: u64 }. Dérivé, publié au front. Invariant : un agent passe àBusyà l'enqueue qui démarre un tour ; revientIdlesur retour-de-prompt OU signal explicite (cf. §6) ; en cas de doute, resteBusymais la file continue d'accepter (forward, jamais bloquer l'émetteur).
WaitForGraph (VO pur — détection de cycle)
domain/src/conversation.rs: structure purewait_edges: Vec<(AgentId, AgentId)>(« A attend B »). Fonction purewould_cycle(graph, from, to) -> bool.- Invariant : une
AskAgentdeAversBest refusée (MailboxError/AppErrortypé) si elle crée un cycle dans le graphe d'attente (A→B alors que B→…→A). 100 % testable sans I/O.
1.2 Invariants transverses
- 1 session vivante / conversation (remplace « 1 / agent »).
session_for(conversation)est déterministe ;sessions_for_agent(agent)peut renvoyer N (une par fil actif). - 1 tâche traitée à la fois / agent : l'
InputMediatorsérialise l'entrée. Deux fils d'un même agent partagent la même file d'entrée (le process CLI sous-jacent est unique — « 1 agent = 1 employé »). Conséquence assumée : un agent occupé par son fil User retarde une délégation entrante — c'est voulu (un employé, une tâche). - Séparation stricte des contextes : écrire dans la conversation
A↔Bne touche jamaisUser↔B. Garanti par le fait que la session reprise (resumable_id) est par conversation, pas par agent.
2. Ports (traits domaine)
Signatures conceptuelles. « Consommé par » = application ; « Implémenté par » = infra/app-tauri.
ConversationRegistry (NOUVEAU — domaine, conversation.rs)
- Rôle : résoudre/ouvrir paresseusement une conversation pour une paire, tenir son
session/resumable_id, suspendre/reprendre.
trait ConversationRegistry: Send + Sync {
/// Get-or-create paresseux : retourne le fil de la paire {a,b}, en l'ouvrant
/// (Dormant) s'il n'existait pas. Pur registre — n'ouvre AUCUNE session.
fn resolve(&self, a: ConversationParty, b: ConversationParty) -> Conversation;
/// Marque une conversation Live avec la session donnée.
fn bind_session(&self, id: ConversationId, session: SessionRef);
/// Suspend : passe Dormant, conserve le resumable_id rendu par la CLI.
fn suspend(&self, id: ConversationId, resumable_id: Option<String>);
fn get(&self, id: ConversationId) -> Option<Conversation>;
}
- Consommé par :
OrchestratorService(au lieu desession_for_agentbrut),LaunchAgent, la reprise au redémarrage. - Implémenté par :
InMemoryConversationRegistry(infra) —HashMap+ mutex sync, jamais tenu en travers d'un.await(cf.ask_locksexistant).
InputMediator (NOUVEAU — domaine ou application ; décision : domaine, input.rs)
- Rôle : le point de convergence de toutes les entrées d'un agent (FIFO unique),
avec
enqueue(Envoyer) etpreempt(Interrompre) distincts, plus l'état busy.
trait InputMediator: Send + Sync {
/// Envoyer = enqueue : ajoute la tâche en queue FIFO de l'agent, retourne le
/// PendingReply à attendre (réutilise le type mailbox existant).
fn enqueue(&self, agent: AgentId, ticket: Ticket) -> PendingReply;
/// Interrompre = préempte : signale au tour en cours de s'arrêter (Échap/stop).
/// N'est PAS un enqueue ; ne corrèle aucun ticket.
fn preempt(&self, agent: AgentId);
/// Marque l'agent libre (retour-de-prompt ou signal explicite) ⇒ avance la file.
fn mark_idle(&self, agent: AgentId);
fn busy_state(&self, agent: AgentId) -> AgentBusyState;
}
- Décision frontière :
InputMediatorabsorbeAgentMailbox. Le mailbox existant devient le moteur de corrélation par ticket interne à l'implémentation duInputMediator(l'InMemoryMailboxest réutilisé tel quel, sa FIFO +oneshotsont exactement ce qu'il faut). On n'a donc pas deux files concurrentes :ask_locks(verrou de tour) +InMemoryMailbox(slots de réponse) sont unifiés derrière ce port. (Voir §5 pour le chemin de migration.) - Consommé par :
OrchestratorService::ask_agent(source =Agent), et le nouveau use caseSubmitHumanInput(source =Human). - Implémenté par :
MediatedInbox(infra) composantInMemoryMailbox+ le registre de verrous de tour + l'état busy.
FileGuard (NOUVEAU — domaine, fileguard.rs)
- Rôle : verrou lecteurs/écrivain par fichier sur le périmètre borné
(contexte
.md+ mémoire). N lecteurs OU 1 écrivain ; mono-écrivain pour le contexte global (l'orchestrateur).
enum GuardedResource { // VO — le périmètre borné, fermé
AgentContext(AgentId),
ProjectContext, // mono-écrivain : orchestrateur uniquement
Memory(MemorySlug),
}
trait FileGuard: Send + Sync {
async fn acquire_read(&self, who: ConversationParty, res: GuardedResource)
-> Result<ReadLease, GuardError>;
async fn acquire_write(&self, who: ConversationParty, res: GuardedResource)
-> Result<WriteLease, GuardError>;
}
ReadLease/WriteLease= gardes RAII (libèrent à la fin de portée).GuardErrortypé :Busy(attendre),Forbidden(un agent ≠ orchestrateur veut écrireProjectContext⇒ refus, doit proposer).- Invariant clé : toute lecture/écriture des ressources gardées transite par ce port ; l'accès fs brut à ces chemins est retiré aux agents (cf. §3 outils MCP).
- Consommé par :
UpdateAgentContext,MemoryStore-consumers, les nouveaux use casesReadContext/ProposeContext/ReadMemory/WriteMemory. - Implémenté par :
RwFileGuard(infra) —HashMap<GuardedResource, RwLock-like>(tokioRwLockou sémaphore), + la règle mono-écrivain pourProjectContext.
AgentMailbox (existant — conservé, statut révisé)
- Reste le contrat de rendez-vous par ticket (corrélation par
TicketId, voir §3.3 — on abandonne la corrélation purement positionnelle « tête de file » dès qu'un agent peut avoir plusieurs fils). Devient un détail d'implémentation duInputMediator; n'est plus injecté seul dansOrchestratorService.
Ports inchangés réutilisés
PtyPort(écriture du tour dans le PTY = désormais le seul chemin d'écriture, piloté par leInputMediator, plus parask_agentdirectement).ProfileStore(porte le motif de retour-de-prompt par profil, §6).EventBus(publieAgentBusyChanged,AgentReplied).
3. Adapters (infra) + outils MCP
3.1 Adapters
| Port | Adapter | Notes |
|---|---|---|
ConversationRegistry |
InMemoryConversationRegistry |
HashMap<ConversationId, Conversation> + index paire→id ; mutex sync. |
InputMediator |
MediatedInbox |
compose InMemoryMailbox (existant) + verrous de tour + état busy ; publie AgentBusyChanged. |
FileGuard |
RwFileGuard |
RwLock par GuardedResource ; règle mono-écrivain ProjectContext. |
AgentMailbox |
InMemoryMailbox |
inchangé (réutilisé sous MediatedInbox). |
3.2 Nouveaux outils MCP (infrastructure/src/orchestrator/mcp/tools.rs)
Ajouts purement additifs au catalogue() (Open/Closed — le dispatch reste intact) :
idea_context_read { target? }→ action wirecontext.read→OrchestratorCommand::ReadContext { target }.targetabsent = le contexte global projet ; sinon le.mdd'un agent. Passe parFileGuard::acquire_read.idea_context_propose { target?, content }→context.propose→OrchestratorCommand::ProposeContext. Pour un agent : écriture directe sous verrou écrivain. Pour le global : ce n'est pas une écriture, c'est une proposition (déposée pour validation par l'orchestrateur/UI ;FileGuardrefuse l'écriture directe avecForbidden).idea_memory_read { slug? }→memory.read→ReadMemory(sousFileGuard).idea_memory_write { slug, content }→memory.write→WriteMemory(verrou écrivain ; mémoire = partagée projet, cf.shared-project-memory).
Chaque outil suit le patron existant : map_tool_call construit un
OrchestratorRequest, validate() reste l'unique autorité de validation, le
requester du handshake porte l'identité (ConversationParty::Agent).
3.3 Corrélation idea_reply par ticket (D)
- Changement : aujourd'hui
idea_replycorrèle positionnellement (tête de la file de l'émetteur —mailbox.resolve(from, result)). Dès qu'un agent peut avoir plusieurs fils, la tête de « sa » file est ambiguë. - Décision : le préfixe injecté dans le PTY (
[IdeA · tâche de A · ticket <id>]) porte déjà leticket_id. On expose un champ optionnelticketau schéma deidea_reply({ result, ticket? }) ; quand présent,resolvecorrèle parTicketId(déterministe, multi-fil) ; absent, on retombe sur la tête de file (compat agents simples, mono-fil). Le préfixe doit donc demander à l'agent de renvoyer leticket(mise à jour de la description outil + protocole §B-5 existant).AgentMailbox::resolvegagne une varianteresolve_ticket(agent, ticket_id, result).
4. Frontière front : vue de sortie (xterm inchangé) / entrée médiée
4.1 État actuel à modifier
frontend/src/features/terminals/TerminalView.tsx câble aujourd'hui directement
les frappes au PTY :
const onKey = term.onData((data) => {
if (handle) void handle.write(encoder.encode(data)); // ← chemin à couper
});
C'est exactement le couplage que le Modèle B retire.
4.2 Décision frontend
- xterm reste la vue de sortie brute, INCHANGÉE :
onData (PTY) → term.writeconservé tel quel. Interdiction de ressusciterAgentChatView(déjà supprimé dans le diff courant — ne pas le réintroduire). term.onData(frappes) n'écrit plus dans le PTY pour une cellule agent. Deux modes :- Cellule terminal simple (non-agent) : comportement actuel conservé (écriture directe — pas de médiation, c'est un shell brut).
- Cellule agent : les frappes vont dans un champ de saisie géré par IdeA
(composant
MediatedInput, rendu sous le terminal), pas dans le PTY. xterm passe en lecture seule pour l'entrée (sortie toujours live).
- Nouveau port UI
InputGateway(frontend/src/ports/index.ts) :Adapter Tauri :interface InputGateway { submit(projectId: string, agentId: string, text: string): Promise<void>; // Envoyer = enqueue interrupt(projectId: string, agentId: string): Promise<void>; // Interrompre = preempt }invoke("submit_agent_input", …)/invoke("interrupt_agent", …)(nouvelles commands app-tauri →SubmitHumanInput/preempt). Mock pour tests. - Occupé/libre remonte par event : un
DomainEvent::AgentBusyChanged { agent_id, busy }relayé en event Tauri (pas un Channel haute-fréquence — événement discret). LeMediatedInputdésactive « Envoyer » pendantBusymais autorise toujours l'enqueue (le bouton enfile derrière ; jamais bloqué — fallback « forward »), et active « Interrompre ». Le front ne parse jamais la sortie pour deviner l'état.
4.3 Composants/state touchés
features/terminals/TerminalView.tsx: brancher le mode agent (entrée détournée).features/terminals/MediatedInput.tsx(nouveau) : champ + boutons Envoyer/Interrompre.features/layout/LayoutGrid.tsx: déjà route versTerminalView; ajoute leMediatedInputsous le terminal quandagent != null.ports/index.ts+adapters/agent.ts(ou nouveladapters/input.ts) + mock.- state : un store léger
agentBusy: Record<agentId, boolean>alimenté par l'event.
5. Impact sur le code existant
5.1 Supprimé / retiré
- L'écriture PTY préfixée par
ask_agent(service.rs~459 :pty.write(&handle, "[IdeA · tâche …]\n")) n'est plus le chemin d'entrée. La tâche déléguée entre désormais parInputMediator::enqueue(qui, dans son impl, écrira la ligne dans le PTY — mais sérialisée derrière l'entrée humaine du même agent, ce qui n'était pas le cas avant). → la logique d'écriture déménage deask_agentvers l'implMediatedInbox. - Band-aid
\n→\r: abandonné (le « mode injection PTV » disparaît). Plus de réécriture de fin de ligne ad hoc. AgentChatView(front) : déjà supprimé dans le diff courant — rester supprimé.
5.2 Modifié
OrchestratorService: ne reçoit pluswith_mailbox(mailbox, pty)séparément maiswith_input_mediator(Arc<dyn InputMediator>)+with_conversations(Arc<dyn ConversationRegistry>).ask_agentdevient : résoudre la conversation A↔B (paresseux), vérifier le graphe wait-for (refus si cycle),enqueuela tâche (source =Agent),await PendingReplyborné.replycorrèle par ticket (§3.3).ensure_live_ptyreste, mais branché sursession_for(conversation)au lieu desession_for_agent.session_for_agent(registreterminal/registry.rs) : devientsession_for(conversation_id);sessions_for_agent(pluriel) ajouté. Lève l'ambiguïtésession-registry-agent-ambiguitypar construction (la clé est la conversation, pas l'agent).bind_endpoint(state.rs) : déjàreclaim_name(true)⇒ unlink du cadavre. Action = verrouiller par un test (ouvrir/fermer/SIGKILL simulé/rebind sansEADDRINUSE). Pas de changement de code attendu, sauf si le test révèle un trou.idea_reply(tools.rs / orchestrator.rs / server.rs) : champticket?ajouté,Reply { from, ticket: Option<TicketId>, result },map_tool_callle propage.Ticket(mailbox.rs) : champssource: InputSource,conversation: ConversationIdajoutés (constructeurs additifs).
5.3 Ajouté
- Domaine :
conversation.rs(ConversationId,Conversation,ConversationParty,ConversationSession,WaitForGraph),input.rs(InputMediator,InputSource,AgentBusyState),fileguard.rs(FileGuard,GuardedResource, leases). - Application : use cases
SubmitHumanInput,ReadContext/ProposeContext,ReadMemory/WriteMemory; détection de cycle câblée dansask_agent. - Infra :
InMemoryConversationRegistry,MediatedInbox,RwFileGuard; outils MCPidea_context_*/idea_memory_*. - app-tauri : commands
submit_agent_input,interrupt_agent; relais eventAgentBusyChanged; câblage des nouveaux ports au composition root (state.rs). - Front :
MediatedInput,InputGateway+ adapter + mock + store busy.
6. Détection occupé/libre
Mécanisme retenu = double signal, OR, avec fallback sûr.
| Signal | Source | Fiabilité |
|---|---|---|
| Retour-de-prompt | motif (regex/literal) déclaré dans le profil CLI (AgentProfile, nouveau champ prompt_ready_pattern: Option<String>), détecté sur le flux PTY par l'impl MediatedInbox |
bon pour un shell/CLI au prompt stable ; faillible (motif dans la sortie) |
| Signal explicite | l'agent appelle idea_reply (fin d'une délégation) ou un signal de fin-de-tour MCP |
déterministe quand l'agent coopère |
- Transition
Busy → Idle= premier des deux signaux qui arrive. - Fallback « en cas de doute → forwarder » : si aucun signal n'est sûr (motif
absent du profil, agent muet), l'agent reste marqué
Busymais la file continue d'accepter lesenqueue; un message entrant n'est jamais rejeté, il patiente dans la FIFO. On ne « piège » donc jamais un message ; au pire il attend. - Garde-fou anti-blocage : le timeout par tour (
ASK_AGENT_TIMEOUT, existant) retire le ticket de tête et relâche le tour même si aucun signal n'est venu ⇒ la file avance. L'agent reste vivant. - Le motif vit dans le profil (donnée, pas code) ⇒ ajouter une CLI = éditer un profil (Open/Closed, cohérent §9 CLAUDE.md).
7. Découpage en lots livrables (ordonnés par dépendance)
Chaque lot = binôme dev/test. B = DevBackend (Rust), F = DevFrontend (TS/React). Chemin critique : C1 → C2 → C3 → C4. FileGuard (C6) et front (F1/F2) parallélisables.
Bloc Conversation (cœur — backend)
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| C1 | B (domaine) | conversation.rs : ConversationId, ConversationParty, Conversation, ConversationSession, WaitForGraph::would_cycle. input.rs : InputSource, AgentBusyState. Extension Ticket (source+conversation, ctors additifs). |
invariants paire (left≠right, ≤1 User) ; identité = paire non ordonnée ; would_cycle (A→B→A refusé, A→B→C ok) ; ticket porte source+conversation. Pur, sans I/O. |
| C2 | B (domaine+infra) | Ports ConversationRegistry + InputMediator (domaine) ; adapters InMemoryConversationRegistry + MediatedInbox (compose InMemoryMailbox existant). |
resolve paresseux (même paire ⇒ même id) ; enqueue→PendingReply ; preempt distinct d'enqueue ; busy_state transitions ; 2 enqueue même agent sérialisés ; agents ≠ parallèles. |
| C3 | B (application) | OrchestratorService : with_input_mediator+with_conversations ; ask_agent réécrit (résout conversation A↔B, garde wait-for, enqueue source=Agent, await) ; reply par ticket. session_for(conversation). Retrait écriture PTY directe + band-aid \r. |
ask A→B route dans la bonne conversation (pas User↔B) ; cycle A→B→A ⇒ erreur typée avant deadlock ; reply corrèle par ticket (multi-fil) ; reply sans ticket = fallback tête ; timeout libère file, cible vivante. |
| C4 | B (application+app-tauri) | Use case SubmitHumanInput (source=Human) + commands submit_agent_input/interrupt_agent ; event AgentBusyChanged relayé. Câblage composition root (state.rs). |
submit humain enfile dans la même FIFO que les délégations ; interrupt = preempt (pas enqueue) ; busy event émis aux bons moments ; câblage : un ask et un submit concurrents sur A sérialisent. |
Bloc détection occupé/libre (backend)
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| C5 | B (domaine+infra) | Champ profil prompt_ready_pattern ; détection retour-de-prompt dans MediatedInbox ; OR avec signal explicite ; fallback « reste Busy mais accepte ». |
motif détecté ⇒ Idle ; idea_reply ⇒ Idle ; ni l'un ni l'autre ⇒ Busy mais enqueue accepté ; timeout ⇒ file avance. |
Bloc FileGuard (backend — parallélisable après C1)
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| C6 | B (domaine+infra) | fileguard.rs (port + GuardedResource + leases) ; RwFileGuard ; règle mono-écrivain ProjectContext. |
N lecteurs concurrents OK ; 1 écrivain exclusif ; agent≠orchestrateur écrit ProjectContext ⇒ Forbidden ; lease RAII libère. |
| C7 | B (application+infra MCP) | Use cases ReadContext/ProposeContext/ReadMemory/WriteMemory sous FileGuard ; outils MCP idea_context_*/idea_memory_* ; retrait accès fs brut de ces chemins. |
map_tool_call → command ; validate exige content ; propose global ≠ write direct ; lecture concurrente non bloquante ; écriture sérialisée. |
Bloc frontend
| Lot | Côté | Périmètre | Tests (Vitest/RTL, gateways mock) |
|---|---|---|---|
| F1 | F | InputGateway (port+adapter+mock) ; MediatedInput (Envoyer=submit / Interrompre=interrupt) ; store busy alimenté par event. |
submit appelle gateway.submit ; interrupt appelle interrupt ; busy event désactive Envoyer (mais enqueue possible), active Interrompre. |
| F2 | F | TerminalView mode agent : frappes → MediatedInput (plus le PTY) ; xterm reste sortie live INCHANGÉE pour le non-agent. LayoutGrid monte MediatedInput sous le terminal si agent != null. |
cellule agent ⇒ onData ne write pas le PTY ; cellule simple ⇒ comportement actuel ; sortie PTY toujours peinte ; jamais d'AgentChatView. |
Bloc durcissement
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| D1 | B (app-tauri) | Test de non-régression bind_endpoint : bind → drop (SIGKILL simulé : laisser le fichier socket) → rebind sans EADDRINUSE. Verrouille reclaim_name(true). |
rebind après cadavre OK ; idempotent ; pas de fuite de fichier après close. |
Ordre recommandé : C1 → C2 → C3 → C4 (cœur), C5 après C2, C6 → C7 en parallèle (après C1), F1 → F2 dès que les commands C4 existent (mock avant), D1 isolé n'importe quand.
8. Stratégie de tests par couche
| Couche | Type | Comment |
|---|---|---|
domaine (conversation, input, fileguard, mailbox étendu) |
unitaires purs, sans I/O ni async là où possible | invariants de paire, would_cycle, transitions AgentBusyState, ctors Ticket. Déterministe. C'est là que vit la garantie « solide par construction ». |
application (OrchestratorService, SubmitHumanInput, use cases FileGuard) |
unitaires avec ports mockés (fakes manuels, façon service.rs actuel) |
ask route la bonne conversation ; cycle refusé ; reply par ticket ; submit+ask sérialisés ; FileGuard mono-écrivain. Aucun vrai PTY/fs/MCP. |
infra (MediatedInbox, RwFileGuard, InMemoryConversationRegistry, outils MCP) |
intégration ciblée | FIFO réelle + oneshot ; RwLock concurrence ; map_tool_call round-trip ; bind_endpoint (D1). Réutilise les tests InMemoryMailbox existants. |
| app-tauri | commands ↔ use cases | submit_agent_input/interrupt_agent mappent bien ; event AgentBusyChanged émis ; câblage composition root cohérent (endpoint partagé). |
frontend (MediatedInput, TerminalView) |
Vitest + RTL, gateways mock | entrée détournée hors PTY ; busy désactive Envoyer sans bloquer enqueue ; xterm sortie inchangée ; sans backend. |
9. Risques / points ouverts
-
Fiabilité de la détection retour-de-prompt (C5) — le plus dur. Un motif dans la sortie d'un agent peut faussement signaler Idle (libère trop tôt) ou ne jamais matcher (reste Busy). Mitigation : OR avec le signal explicite
idea_reply+ fallback « reste Busy mais accepte » + timeout par tour. Reste ouvert : faut-il un « heartbeat » MCP de fin-de-tour côté CLI ? (hors périmètre immédiat, Claude-only). -
Suspension/reprise de session par conversation (
resumable_id) — un agent à N fils doit reprendre le bon session-id par fil au redémarrage. Dépend dusession{assignFlag,resumeFlag}du profil (cf.conversation-resume-architecture). Ouvert : capacité réelle des CLI à tenir N conversations resumables simultanées pour un même process « 1 agent = 1 employé » — possible conflit entre « N fils » et « 1 process ». Décision de cadrage : 1 process/agent, les fils partagent la file d'entrée (sérialisés) ; leresumable_idpar conversation sert surtout à la reprise au redémarrage, pas à du vrai parallélisme intra-process. -
Deadlock & détection de cycle (
WaitForGraph) — couvre A→B→A directs et transitifs, mais le graphe doit être alimenté en temps réel (arête posée à l'enqueue, retirée au reply/timeout). Risque : arête fantôme si un reply se perd ⇒ faux positif de cycle. Mitigation : retrait d'arête garanti par le RAII du tour (comme_turnaujourd'hui) + timeout. -
Corrélation par ticket vs agents « simples » — un agent qui ne renvoie pas le
ticketdansidea_replyretombe sur la corrélation positionnelle (tête de file), ambiguë en multi-fil. Mitigation : protocole §B-5 (description outil) insiste sur le renvoi du ticket ; mono-fil reste correct sans. Ouvert : forcer le ticket requis casserait des agents simples — on garde optionnel. -
Périmètre FileGuard contournable — tant que l'agent garde un shell brut (PTY), il peut écrire les
.md/mémoire par le filesystem malgré le verrou MCP. Le verrou n'est étanche que si l'accès fs à ces chemins est réellement retiré (sandbox, cf.agent-permissions-architecture/ Landlock). Ouvert : sans sandbox OS, leFileGuardest coopératif (protège des collisions IdeA↔IdeA, pas d'un agent qui contourne). À acter : FileGuard = correction des collisions dans le chemin IdeA d'abord ; étanchéité réelle = lot sandbox ultérieur. -
Migration
AgentMailbox→InputMediator— risque de double-file transitoire. Mitigation :MediatedInboxenveloppeInMemoryMailbox(pas de réécriture),OrchestratorServicebascule d'unwith_mailboxverswith_input_mediatoren un lot (C2→C3), tests existantsInMemoryMailboxconservés verts.
Document maintenu par l'Agent Architecture — cadrage « conversation par paire », base des lots C1→C7 / F1→F2 / D1 avant tout code.