Files
IdeA/.ideai/briefs/d6-messagerie-inter-agents.md
Blomios dd1194abe8 feat(agent): messagerie inter-agents via send_blocking (D6) — §17
Comble le trou historique : un agent qui en interroge un autre reçoit enfin
le CONTENU de la réponse, plus seulement un ACK de cycle de vie.

- domain : OrchestratorCommand::AskAgent { target, task } + action wire
  agent.message (target+task requis) ; DomainEvent::AgentReplied
  { agent_id, reply_len } (bus I/O-free, le contenu remonte par l'outcome).
- application : OrchestratorOutcome gagne reply: Option<String>. Méthode
  ask_agent (§17.4) — cible structurée vivante ⇒ send_blocking direct ;
  cible morte ⇒ LaunchAgent structuré puis send ; agent vivant en PTY ou
  profil sans structured_adapter ⇒ Invalid (non adressable, jamais d'ACK
  trompeur) ; agent inconnu ⇒ NotFound ; service non câblé ⇒ Invalid.
  Timeout (300s) ⇒ erreur typée SANS tuer la session. Succès ⇒ publie
  AgentReplied + reply: Some(content). Injection additive via builders
  with_structured / with_events (call sites legacy intacts).
- app-tauri : state câble with_structured/with_events ; DTO AgentReplied.

Tests (QA) : +14 verts (11 app + 3 domaine), invariants validés par
mutation test (reply!=None, timeout ne shutdown pas). cargo test
--workspace : 817 passed, 0 failed.

Suivi (D6b) : surfacer reply dans le writer wire .response.json
(infrastructure/orchestrator) pour la délégation par protocole fichier.
Reste D7 : menu restreint Claude/Codex + retrait custom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:44:53 +02:00

6.9 KiB

Brief Dev — Lot D6 : messagerie inter-agents via send_blocking (§17.9)

Demandé par Main à DevBackend (dev) + QA (test). Cycle §3 : code → tests → vert. Périmètre backend (domaine + application). Pas de frontend.

0. Le trou que D6 comble (important)

Aujourd'hui, « Main demande à Architect » ne retourne jamais le contenu de la réponse :

  • OrchestratorCommand n'a pas de variante « demander/attendre une réponse ». Le wire agent.run replie task dans context, et context n'est utilisé que pour un agent neuf (.md initial) ⇒ pour un agent déjà existant, le task est silencieusement ignoré.
  • La réponse (*.response.json) ne porte qu'un ACK de cycle de vie (detail: "launched agent X"), jamais la sortie produite par la cible.

D6 = brancher la messagerie synchrone sur le port AgentSession : la cible est pilotée en mode structuré, on attend son tour et on renvoie son contenu. La primitive existe déjà : crate::agent::structured::send_blocking(session, prompt, timeout) -> Result<String, AgentSessionError> (livrée en D1 ; retourne le contenu du Final, Timeout typé sans tuer la session).

1. Périmètre D6 (réf. tableau §17.9 ligne D6)

A. Domaine (crates/domain/src/)

  1. OrchestratorCommand::AskAgent { target: String, task: String } (orchestrator.rs, enum ~l.111). Nouvelle variante « j'attends une réponse ».
  2. Validation : nouvelle action wire agent.messageAskAgent dans OrchestratorRequest::validate (~l.175). targetAgent (ou name) et task requis non-vides (sinon OrchestratorError::MissingField). Garde le mapping agent.run actuel inchangé (lancement fire-and-forget). Ajoute un cas de test de round-trip JSON agent.message.
  3. DomainEvent::AgentReplied { ... } (events.rs, à côté de AgentLaunched) pour l'observabilité : au minimum le nom/id de l'agent cible et, si pertinent, la taille/preview de la réponse (reste pur, pas de payload lourd imposé — calque le style des variantes voisines).

B. Application (crates/application/src/orchestrator/service.rs)

  1. OrchestratorOutcome gagne le contenu : ajoute reply: Option<String> (l'ACK actuel detail reste). Les commandes existantes mettent reply: None ; AskAgent met reply: Some(contenu). (Champ additif ⇒ aucune régression des call sites/tests existants.)
  2. dispatch route AskAgent :
    • résous l'agent cible par nom (find_agent_id_by_name) → sinon AppError::NotFound typé.
    • cherche sa session structurée vivante dans le registre StructuredSessions (PAS TerminalSessions). Si vivante ⇒ send_blocking(session, &task, timeout).
    • si pas vivante ⇒ lance l'agent en mode structuré (via LaunchAgent, background) puis send_blocking. Respecte l'invariant 1 session/agent.
    • si la cible est PTY-only (profil sans structured_adapter ⇒ pas adressable en ask) ⇒ erreur typée explicite (AppError::Invalid/NotFound avec message clair « agent X n'est pas pilotable en mode structuré »). C'est acceptable (le menu ne crée plus que des agents structurés, cf. D7 à venir).
    • timeout ⇒ remonte une erreur typée, sans tuer la session (déjà la sémantique de send_blocking).
    • en cas de succès ⇒ publie DomainEvent::AgentReplied sur l'EventBus, et retourne OrchestratorOutcome { detail, reply: Some(content) }.
  3. Injection : le service a besoin du registre StructuredSessions + de l'EventBus (et de quoi piloter send_blocking). Ajoute-les par builder additif (with_structured(...) / with_events(...) façon D3 sur LaunchAgent) pour que OrchestratorService::new reste compatible et que les tests/call sites legacy restent verts. Câble au composition root (crates/app-tauri/src/state.rs).

C. Nettoyage voie principale

  1. Aucun accès outbox/inbox dans le chemin AskAgent (le rendez-vous est intrinsèque à send_blocking). Les seules occurrences outbox/inbox actuelles sont des commentaires de doc dans agent/structured.rs — ne ré-introduis rien. Vérifie qu'aucun AgentReplyChannel/outbox n'est utilisé.

2. Invariants à respecter

  • 1 session vivante par agent across registres (PTY + structuré).
  • Timeout ne tue jamais la session (retry possible).
  • Cible PTY non adressable par ask ⇒ erreur typée, jamais un ACK trompeur ni un panic.
  • Frontières hexagonales : le domaine reste pur (pas d'I/O dans orchestrator.rs/events.rs) ; l'orchestration vit dans l'application ; aucun new d'adapter infra dans le service.
  • Zéro régression : agent.run/spawn_agent/stop_agent/update_agent_context/skill.create inchangés ; le watcher d'orchestration et ses tests restent verts.

3. Tests attendus (QA — colonne « Tests attendus » D6 du §17.9)

Unitaires avec fakes (fake AgentSession, fake registres, fake EventBus — aucun vrai CLI) :

  • cible vivante (session structurée enregistrée) ⇒ send_blocking appelé, reply: Some(...) porte le contenu du Final.
  • cible morteLaunchAgent invoqué (structuré) puis send ; reply renvoyé.
  • timeout ⇒ erreur typée remontée, session non tuée (le fake atteste qu'aucun shutdown n'a été appelé), pas de reply.
  • AgentReplied publié sur l'EventBus en cas de succès.
  • cible PTY-only (profil sans adapter / présente seulement dans TerminalSessions) ⇒ erreur typée explicite, pas d'ACK « launched ».
  • validation : agent.message sans taskMissingField ; round-trip JSON agent.message.
  • non-régression : agent.run ne change pas de comportement ; aucun accès outbox.
  • garde anti-always-green sur au moins l'invariant timeout-ne-tue-pas OU ask-retourne-le-contenu.

4. Méthode

DevBackend code → vérifie cargo build -p domain -p application -p app-tauri. N'exécute pas cargo test --workspace si un build concurrent tient le lock (sinon, lance-le). QA écrit + exécute les tests, produit un rapport clair si rouge. Ne pas committer, ne pas push — Main relit et commit.

5. Références

  • Domaine : crates/domain/src/orchestrator.rs (enum OrchestratorCommand ~l.111, validate ~l.175), crates/domain/src/events.rs (DomainEvent).
  • Application : crates/application/src/orchestrator/service.rs (struct/deps ~l.37, dispatch ~l.88, OrchestratorOutcome ~l.50, spawn_agent comme modèle de résolution d'agent).
  • Primitive : crates/application/src/agent/structured.rs (send_blocking).
  • Registre structuré : StructuredSessions (livré D1, jumeau de TerminalSessions) — repère-le et lis son API avant de t'en servir.
  • Composition root : crates/app-tauri/src/state.rs.
  • Spec : ARCHITECTURE.md §17.4 (réconciliation §16) et tableau §17.9 ligne D6.