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>
This commit is contained in:
104
.ideai/briefs/d6-messagerie-inter-agents.md
Normal file
104
.ideai/briefs/d6-messagerie-inter-agents.md
Normal file
@ -0,0 +1,104 @@
|
||||
# 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.message`** → `AskAgent` 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`)
|
||||
4. **`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.)
|
||||
5. **`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) }`.
|
||||
6. **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
|
||||
7. **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 **morte** ⇒ `LaunchAgent` 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 `task` ⇒ `MissingField` ; 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**.
|
||||
Reference in New Issue
Block a user