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>
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 :
OrchestratorCommandn'a pas de variante « demander/attendre une réponse ». Le wireagent.runreplietaskdanscontext, etcontextn'est utilisé que pour un agent neuf (.mdinitial) ⇒ pour un agent déjà existant, letaskest 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/)
OrchestratorCommand::AskAgent { target: String, task: String }(orchestrator.rs, enum ~l.111). Nouvelle variante « j'attends une réponse ».- Validation : nouvelle action wire
agent.message→AskAgentdansOrchestratorRequest::validate(~l.175).targetAgent(ouname) ettaskrequis non-vides (sinonOrchestratorError::MissingField). Garde le mappingagent.runactuel inchangé (lancement fire-and-forget). Ajoute un cas de test de round-trip JSONagent.message. DomainEvent::AgentReplied { ... }(events.rs, à côté deAgentLaunched) 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)
OrchestratorOutcomegagne le contenu : ajoutereply: Option<String>(l'ACK actueldetailreste). Les commandes existantes mettentreply: None;AskAgentmetreply: Some(contenu). (Champ additif ⇒ aucune régression des call sites/tests existants.)dispatchrouteAskAgent:- résous l'agent cible par nom (
find_agent_id_by_name) → sinonAppError::NotFoundtypé. - cherche sa session structurée vivante dans le registre
StructuredSessions(PASTerminalSessions). Si vivante ⇒send_blocking(session, &task, timeout). - si pas vivante ⇒ lance l'agent en mode structuré (via
LaunchAgent, background) puissend_blocking. Respecte l'invariant 1 session/agent. - si la cible est PTY-only (profil sans
structured_adapter⇒ pas adressable enask) ⇒ erreur typée explicite (AppError::Invalid/NotFoundavec 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::AgentRepliedsur l'EventBus, et retourneOrchestratorOutcome { detail, reply: Some(content) }.
- résous l'agent cible par nom (
- Injection : le service a besoin du registre
StructuredSessions+ de l'EventBus(et de quoi pilotersend_blocking). Ajoute-les par builder additif (with_structured(...)/with_events(...)façon D3 surLaunchAgent) pour queOrchestratorService::newreste 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
- Aucun accès outbox/inbox dans le chemin
AskAgent(le rendez-vous est intrinsèque àsend_blocking). Les seules occurrencesoutbox/inboxactuelles sont des commentaires de doc dansagent/structured.rs— ne ré-introduis rien. Vérifie qu'aucunAgentReplyChannel/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 ; aucunnewd'adapter infra dans le service. - Zéro régression :
agent.run/spawn_agent/stop_agent/update_agent_context/skill.createinchangé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_blockingappelé,reply: Some(...)porte le contenu duFinal. - cible morte ⇒
LaunchAgentinvoqué (structuré) puissend;replyrenvoyé. - timeout ⇒ erreur typée remontée, session non tuée (le fake atteste qu'aucun
shutdownn'a été appelé), pas dereply. AgentRepliedpublié 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.messagesanstask⇒MissingField; round-trip JSONagent.message. - non-régression :
agent.runne 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(enumOrchestratorCommand~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_agentcomme modèle de résolution d'agent). - Primitive :
crates/application/src/agent/structured.rs(send_blocking). - Registre structuré :
StructuredSessions(livré D1, jumeau deTerminalSessions) — 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.