Files
IdeA/.ideai/briefs/orchestration-v3-cadrage.md
Blomios 37e72747d3 feat(agent): orchestration v3 — surface MCP model-agnostic (M0→M4) — §14.3
Expose l'orchestration IdeA comme serveur MCP par-dessus le même
OrchestratorService::dispatch, avec repli fichier .ideai/requests pour les
CLI sans MCP. v3 réduite à la surface MCP : la messagerie inter-agents et la
corrélation requête↔réponse étaient déjà résolues par §17 (send_blocking).

- M0 capacité MCP sur le profil (McpCapability/McpConfigStrategy/McpTransport)
- M1 injection conf MCP au LaunchAgent + prose adaptée selon la surface
- M2 serveur/adapter MCP (JSON-RPC 2.0 maison ; outils idea_*) + ListAgents
- M3 câblage par projet (registre mcp_servers jumeau du watcher)
- M4 observabilité UI : OrchestratorRequestProcessed.source = file|mcp + badge

Trois portes d'entrée (fichier, MCP, UI) → un seul dispatch ; aucun nouveau
port applicatif ; MCP confiné à l'adapter infra. Tous lots verts (cycle §3).
Cadrage : .ideai/briefs/orchestration-v3-cadrage.md ; ARCHITECTURE.md §14.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:53:31 +02:00

22 KiB

Cadrage Architecture — Orchestration v3 : invocation native d'agents (surface MCP)

Produit par Architect en réponse au brief orchestration-v3-invocation-native.md. Cadrage avant tout code (méthode §3). Livrable : ce document + mise à jour ARCHITECTURE.md §14.3. Aucun code de production ici.


0. État réel du terrain — ce qui est DÉJÀ résolu (lu dans le code, pas présumé)

Le brief décrit trois faiblesses (conscience = prose, pas de discussion inter-agents, fire-and-forget). Deux des trois sont déjà comblées par le pivot §17 (livré, lots D0→D7). Il faut le constater honnêtement pour ne pas re-cadrer ce qui existe :

Faiblesse du brief Statut réel Référence code
Pas de discussion inter-agents (agent.message « future », task ignoré pour agent vivant) RÉSOLU OrchestratorCommand::AskAgent (domain/src/orchestrator.rs), OrchestratorService::ask_agent (application/src/orchestrator/service.rs)
Fire-and-forget, pas de corrélation requête↔réponse RÉSOLU sans outbox : le rendez-vous synchrone est intrinsèque à AgentSession::send() (flux → Final déterministe). Le Final est la fin de tour. application/src/agent/structured.rs (send_blocking), domain/src/ports.rs (ReplyEvent::Final)
Pas de réveil du demandeur / event de réponse RÉSOLU DomainEvent::AgentReplied, OrchestratorResponse.reply (infrastructure/src/orchestrator/mod.rs)
Conscience = soft prompt (l'agent doit deviner le schéma JSON) ⚠️ PARTIEL : la prose # Orchestration IdeA est injectée (compose_convention_file), mais aucun outil typé natif n'est exposé. application/src/agent/lifecycle.rs
Interdiction des subagents natifs + alternative native ⚠️ PARTIEL : interdiction présente (prose) ; l'alternative native (outils idea_*) manque encore. idem
Capacité MCP sur le profil ABSENT
Serveur MCP / config MCP par CLI ABSENT

Conclusion de cadrage : l'orchestration v3 n'est plus « combler la messagerie inter-agents » (c'est fait). Elle se réduit à un seul chantier net : exposer l'orchestration IdeA comme serveur MCP model-agnostic, en tant qu'adapter entrant supplémentaire par-dessus le même OrchestratorService::dispatch, avec repli homogène sur le protocole fichier .ideai/requests (§14.3) + prose (compose_convention_file) pour les CLI sans MCP. C'est ce que cadre la suite.

Principe directeur (zéro régression, §9/§17.3) : MCP est un confort de conscience native (outils typés, plus de schéma à deviner). Il n'invente aucune sémantique : tout outil MCP se ramène à un OrchestratorCommand déjà existant. La voie principale du retour de valeur reste §17 (send_blocking) ; MCP ne fait que déclencher dispatch, jamais re-router la réponse.


1. Décisions tranchées (les 4 points durs du brief)

Décision 1 — Capacité MCP par runtime = champ optionnel mcp sur AgentProfile (Open/Closed)

Tranché : on ajoute un champ optionnel mcp: Option<McpCapability> sur AgentProfile, exactement comme session: Option<SessionStrategy> et structured_adapter: Option<StructuredAdapter> le sont déjà. None (défaut) ⇒ repli fichier + prose (comportement actuel, zéro régression). Some(_) ⇒ IdeA matérialise la config MCP de cette CLI au lancement et l'agent voit les outils idea_*.

// domain/src/profile.rs — capacité MCP déclarative (pur, validé par constructeur, comme SessionStrategy)

/// Stratégie de matérialisation de la config MCP propre à UNE CLI : chaque CLI
/// déclare son serveur MCP différemment (fichier `.mcp.json` pour Claude Code,
/// flag de lancement, ou variable d'env). Déclaratif = donnée, pas code (§9).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "strategy")]
pub enum McpConfigStrategy {
    /// Écrire un fichier de conf MCP au chemin (relatif au run dir isolé §14.1)
    /// attendu par la CLI, au format JSON propre à cette CLI (ex. `.mcp.json`).
    ConfigFile { target: String },          // relative_safe(target) — pas de `..`, pas d'absolu
    /// Passer le serveur via un flag de lancement (ex. `--mcp-config {path}`).
    Flag { flag: String },                  // non_empty(flag)
    /// Passer via une variable d'environnement.
    Env { var: String },                    // valid_env_var(var)
}

/// Capacité MCP d'un profil : COMMENT déclarer le serveur MCP IdeA à cette CLI,
/// et QUEL transport. `None` sur le profil ⇒ repli fichier `.ideai/requests` + prose.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpCapability {
    /// Comment matérialiser la config MCP au lancement (relatif au run dir).
    pub config: McpConfigStrategy,
    /// Transport du serveur MCP IdeA (détail invisible au domaine ; voir D3).
    /// `stdio` = défaut robuste cross-OS ; `socket` = optimisation (point ouvert).
    #[serde(default)]
    pub transport: McpTransport,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum McpTransport { #[default] Stdio, Socket }

Sur AgentProfile, additif et non cassant (sérialisation inchangée pour les profils sans MCP) :

#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp: Option<McpCapability>,

Builder additif (comme with_structured_adapter) : AgentProfile::new(...).with_mcp(cap) ; la signature de AgentProfile::new reste inchangée ⇒ tous les appels du catalogue/tests restent verts.

Justification : cohérence §9 (« ajouter une IA = donnée, pas code »), symétrie avec les deux autres capacités optionnelles déjà sur le profil, skip_serializing_if = Nonezéro régression de sérialisation. Le prédicat de surface est profile.mcp.is_some() — un seul point de vérité.

Modèle en couches (exigé par le brief §4.1) : la surface effective d'un agent est surface(agent) = if profile.mcp.is_some() { Mcp } else { FileProtocol }. Les deux couches produisent le même OrchestratorCommand. Aucun agent n'est jamais bloqué : sans MCP, la prose # Orchestration IdeA + .ideai/requests reste pleinement fonctionnelle.


Décision 2 — Retour synchrone d'ask = AUCUN nouveau modèle de corrélation : on réutilise send_blocking

Tranché (et c'est la décision la plus importante) : l'outil MCP idea_ask_agent ne crée aucune corrélation requête↔réponse, aucun outbox, aucun event de réveil neufs. Il appelle le même OrchestratorService::dispatch(AskAgent { target, task }) que le watcher fichier, qui retourne déjà OrchestratorOutcome { reply: Some(content) } via send_blocking. L'adapter MCP renvoie ce content inline comme valeur de retour de l'outil. Fin.

Le brief (rédigé avant le pivot §17) supposait qu'ask était un point dur à résoudre via outbox + corrélation fichier. Le pivot §17 l'a déjà tranché autrement : le Final du ReplyStream est la fin de tour déterministe ; pas besoin de deviner, pas d'outbox, pas de request_id. On n'y revient pas. Le tableau ci-dessous fige la sémantique, déjà implémentée :

Aspect Décision (déjà en place) Code
Corrélation Aucune : dispatch est un appel synchrone async ; la réponse est la valeur de retour. Le transport MCP (JSON-RPC) porte nativement la corrélation requête/réponse. service.rs::ask_agent
Outbox Supprimé de la voie principale (§17.4). Pas réintroduit.
Event AgentReplied Observabilité UI uniquement (« Architect a répondu à Main »), best-effort, ne porte pas la valeur. reply_outcome
Timeout Borné (ASK_AGENT_TIMEOUT = 300 s). À l'expiration : AgentSessionError::Timeout → la cible reste vivante (non tuée), l'outil MCP renvoie une erreur typée ; l'appelant décide. send_blocking
Cible a déjà une session vivante (one-live-session-per-agent) ask réutilise la session structurée vivante (session_for_agent) — rendez-vous direct, pas de respawn. Si la cible est vivante en PTY brut (profil sans structured_adapter) ⇒ erreur typée explicite (jamais un ACK trompeur). service.rs::ask_agent étapes 1→3
Cible morte LaunchAgent en mode structuré (background) puis send_blocking. Garde d'unicité sur les deux registres. idem

Justification : DRY radical (une seule logique de rendez-vous, partagée par UI chat, watcher fichier et MCP) ; frontière nette (le domaine ne connaît qu'un prompt et un Final, jamais un transcript ni un id de corrélation) ; universalité (marche pour toute CLI structurée Claude/Codex). Le seul travail v3 ici est de brancher l'outil MCP sur dispatch — pas de re-cadrer le rendez-vous.

Conséquence produit : idea_ask_agent cible toujours un agent structuré (Claude/Codex), cohérent avec le menu restreint §17.3/§17.6. Un agent demandeur peut être n'importe quelle CLI MCP (Claude, Codex, Gemini…) ; un agent cible d'un ask doit être structuré. C'est déjà l'invariant en vigueur — MCP ne le change pas.


Décision 3 — MCP vs subagents natifs : on garde l'interdiction ET on offre l'alternative native, config injectée par CLI au lancement

Tranché : l'interdiction des subagents natifs (prose # Orchestration IdeA) reste — elle protège l'identité/mémoire/observabilité IdeA. Mais on offre désormais la vraie alternative native : les outils idea_* apparaissent dans la liste d'outils de la CLI. La prose est adaptée selon la surface :

  • agent MCP (profile.mcp.is_some()) : la prose pointe vers les outils idea_ask_agent / idea_launch_agent / idea_list_agents (au lieu d'« écris un JSON dans .ideai/requests »).
  • agent fichier (mcp == None) : prose .ideai/requests actuelle, inchangée.

Injection de la config MCP par CLI = au LaunchAgent, dans le run dir isolé (§14.1), via la McpConfigStrategy — exactement le même point et la même mécanique que le convention file :

LaunchAgent::execute (après apply_injection, avant spawn/factory.start) :
  if let Some(mcp) = &profile.mcp:
      // IdeA matérialise SA config MCP au format de CETTE CLI dans <run_dir>/...
      apply_mcp_config(mcp, &run_dir, &spec)   // ConfigFile→write ; Flag→spec.args ; Env→spec.env
  • ConfigFile { target } : écrit <run_dir>/<target> (ex. .mcp.json) avec la déclaration du serveur MCP IdeA (commande/transport). Non-clobbering, best-effort, comme le seed de permissions.
  • Flag { flag } : ajoute le flag + chemin au SpawnSpec.args.
  • Env { var } : ajoute la variable au SpawnSpec.env.

Le serveur MCP lui-même est démarré par projet ouvert, à côté du FsOrchestratorWatcher, dans le même hook ensure_orchestrator_watch (app-tauri/src/state.rs). Une CLI qui se lance avec la config injectée se connecte à ce serveur (stdio : IdeA spawn un pont par session ; socket : adresse partagée — détail d'adapter, point ouvert S-MCP).

Justification : symétrie totale avec le convention file et le seed de permissions (même run dir, même best-effort non-clobbering, même moment) ⇒ aucune nouvelle plomberie de cycle de vie. La config MCP est donnée déclarative par profil, donc « ajouter une CLI MCP = donnée, pas code ».


Décision 4 — Frontières hexagonales : le serveur MCP est un adapter entrant d'infrastructure ; AUCUN nouveau port applicatif

Tranché : le serveur MCP est un driving adapter d'infrastructure (infrastructure/src/orchestrator/mcp/), pair du FsOrchestratorWatcher. Il appelle le même OrchestratorService::dispatch (application) et ne duplique rien. Trois portes d'entrée substituables se ramènent au même OrchestratorCommand :

                 ┌─────────────────────────────────────────────┐
   Agent MCP ───▶│  Serveur MCP (infra/orchestrator/mcp)        │──┐
                 └─────────────────────────────────────────────┘  │
                 ┌─────────────────────────────────────────────┐  │   OrchestratorCommand
   Fichier   ───▶│  FsOrchestratorWatcher (infra/orchestrator) │──┼──▶ OrchestratorService::dispatch
   (.ideai/      └─────────────────────────────────────────────┘  │   (application — INCHANGÉ)
    requests)    ┌─────────────────────────────────────────────┐  │         │
   UI        ───▶│  Commandes Tauri (app-tauri)                 │──┘         ▼
                 └─────────────────────────────────────────────┘     use cases agent/terminal
  • Où vit le serveur MCP : infrastructure/src/orchestrator/mcp/. Il traduit un appel d'outil MCP (idea_ask_agent, idea_launch_agent, idea_list_agents, et par parité idea_update_context, idea_create_skill, idea_stop_agent) en OrchestratorCommand, appelle dispatch, et renvoie OrchestratorOutcome (reply/detail) inline comme résultat d'outil. JSON-RPC, stdio/socket, le crate MCP : tout reste dans cet adapter. Le domaine/application ignorent MCP.
  • Quel port côté domaine/application : aucun nouveau. OrchestratorService::dispatch (application) est déjà l'unique seam. idea_list_agents réutilise ListAgents. La validation (OrchestratorRequest::validate) reste le point unique « parse, don't validate » — l'adapter MCP construit un OrchestratorCommand (directement, ou via OrchestratorRequest pour réutiliser la validation, au choix d'implémentation).
  • Réutilisation de OrchestratorService plutôt que duplication : le serveur MCP reçoit Arc<OrchestratorService> au composition root (state.rs), exactement comme le watcher. Une seule logique applicative ; les adapters ne portent que leur techno d'entrée.

Justification : DRY + règle de dépendance hexagonale. Cible, identité, mémoire, observabilité UI passent toujours par le seul chemin applicatif. Les spikes MCP (transport, crate) sont confinés à l'adapter infra et ne touchent ni le domaine ni l'application.


2. Modèle de messages corrélés — état figé (rien de neuf)

La « corrélation requête↔réponse » du brief est portée nativement par le transport :

  • MCP : JSON-RPC corrèle requête/réponse par id de message ⇒ rien à modéliser côté IdeA.
  • Fichier : <file>.json<file>.json.response.json (sibling), déjà en place.
  • Valeur de retour : OrchestratorOutcome { detail, reply } (application) → OrchestratorResponse { ok, action, detail, error, reply } (infra fichier) ou résultat d'outil MCP. Structs déjà définies, réutilisées telles quelles.

Aucun CorrelationId, aucun AgentReply, aucun port AgentReplyChannel, aucun outbox : abandonnés par le pivot §17 et non réintroduits par v3. C'est la simplification clé.


3. Découpage en LOTS testables (méthode §3) — MCP uniquement

Chaque lot = binôme dev+test, vert avant le suivant. Backend/frontend séparés. Les terminaux non-IA et le chemin fichier .ideai/requests restent verts à chaque lot. Spike S-MCP (crate MCP Rust + transport stdio/socket + format de conf par CLI) est confiné au lot M2 et n'invalide pas l'ossature (le contrat d'entrée reste OrchestratorCommand).

Lot Côté Périmètre Crates/dossiers Contrats Tests attendus
M0 (capacité profil) back McpCapability + McpConfigStrategy + McpTransport (domaine, validés) ; champ AgentProfile.mcp: Option<McpCapability> (+ builder with_mcp, new inchangé) ; catalogue Claude/Codex annotés (ex. ConfigFile { target: ".mcp.json" }). domain/src/profile.rs, application/src/agent/catalogue.rs enum + struct + champ optionnel sérialisé unit purs : mcp = None round-trip identique à avant (zéro régression sérialisation) ; Some(_) round-trip ; constructeurs valident (relative_safe target, non_empty flag, valid_env_var) ; catalogue annoté.
M1 (injection conf MCP au lancement) back LaunchAgent matérialise la conf MCP selon McpConfigStrategy dans le run dir isolé (après apply_injection, avant spawn/factory.start) : ConfigFile→write non-clobbering, Flagargs, Envenv. Prose compose_convention_file adaptée selon mcp.is_some(). application/src/agent/lifecycle.rs LaunchAgent (chemin MCP additif) unit (fakes) : profil mcp=Noneaucun write/flag/env MCP (chemin actuel inchangé) ; ConfigFile ⇒ fichier écrit au bon chemin, non-clobbering ; Flag/Envspec enrichi ; prose contient les outils idea_* si MCP, sinon .ideai/requests.
M2 (serveur/adapter MCP) back infrastructure/src/orchestrator/mcp/ : serveur MCP exposant idea_ask_agent/idea_launch_agent/idea_list_agents (+ parité idea_update_context/idea_create_skill/idea_stop_agent) → OrchestratorCommanddispatch → résultat inline. Spike S-MCP (crate, transport) isolé ici. infrastructure/src/orchestrator/mcp/ mapping outil→commande ; Arc<OrchestratorService> injecté unit (fakes + OrchestratorService à use cases fakes) : chaque outil mappe la bonne commande ; idea_ask_agent renvoie reply inline ; timeout → erreur typée, cible non tuée ; idea_list_agents liste ; JSON-RPC malformé → erreur, jamais panic. Hors-réseau (transport en mémoire/pipe scriptable).
M3 (câblage par projet) back Démarrer le serveur MCP par projet ouvert dans ensure_orchestrator_watch (à côté du watcher) ; registre mcp_servers jumeau de orchestrator_watchers ; arrêt à la fermeture du projet. app-tauri/src/state.rs, commands.rs hook ensure_orchestrator_watch étendu app-tauri : un serveur MCP par projet, idempotent ; fermeture du projet ⇒ arrêt ; coexiste avec le watcher fichier (les deux portes vivantes).
M4 (observabilité UI — optionnel) front Surfacer dans l'UI Agents qu'une délégation est passée par MCP vs fichier (badge/source sur l'event OrchestratorRequestProcessed / AgentReplied). Non bloquant. frontend/src/features/agents DTO d'event enrichi (`source: "mcp" "file"`)

Ordre conseillé : M0 → M1 → M2 → M3 (→ M4 optionnel). M0 débloque tout (donnée pure) ; M1 injecte la conf (testable sans serveur) ; M2 livre l'adapter derrière un transport scriptable (spike confiné) ; M3 le câble par projet. M4 est du confort d'observabilité.


4. Conformité hexagonale & SOLID (rappel)

  • Règle de dépendance : McpCapability/McpConfigStrategy sont domaine (purs, validés). Le serveur MCP, JSON-RPC, stdio/socket, le crate MCP sont exclusivement infra. Le domaine et l'application ignorent MCP (l'application ne voit que OrchestratorCommand/dispatch).
  • S : le serveur MCP = une seule techno d'entrée (MCP→commande). OrchestratorService garde sa responsabilité (commande→use cases). LaunchAgent gagne une étape d'injection homogène, pas une responsabilité nouvelle.
  • O : ajouter une CLI MCP = un bloc mcp sur le profil (donnée). Aucun cœur touché.
  • L : les trois portes d'entrée (fichier, MCP, UI) sont substituables — même dispatch, même résultat. Repli fichier ≡ MCP du point de vue de la réponse.
  • I : le serveur MCP ne reçoit que Arc<OrchestratorService> (pas les use cases en détail).
  • D : tout injecté au composition root (state.rs) ; aucun new d'adapter MCP ailleurs.

5. Chantiers adjacents (situés, NON cadrés ici)

  • Hot-swap de l'AI profile (chantier A, §15.1) : LIVRÉ (ChangeAgentProfile). Interaction avec v3 : un swap vers/depuis un profil MCP change la surface (mcp.is_some()) ⇒ au relance, LaunchAgent (ré)injecte ou retire la conf MCP automatiquement. Rien à cadrer : la surface suit le profil courant, point de vérité unique.
  • Reprise auto des sessions au redémarrage (chantier B, §15.2) : LIVRÉ (ListResumableAgents, conversation_id persisté sur la cellule). Interaction avec v3 : à la reprise, LaunchAgent ré-matérialise la conf MCP comme à tout lancement (M1). Rien à cadrer.

Ces deux chantiers ne sont pas un prérequis de v3/MCP et n'en bloquent aucun lot.


6. Synthèse des décisions

  1. Capacité MCP = Option<McpCapability> sur le profil (Open/Closed, None ⇒ repli fichier+prose, zéro régression sérialisation).
  2. Retour synchrone d'ask : RIEN de neuf — réutilise send_blocking/AskAgent/AgentReplied/OrchestratorOutcome.reply déjà livrés (§17). Pas d'outbox, pas de corrélation fichier, pas de CorrelationId. Le transport MCP corrèle nativement.
  3. Interdiction subagents natifs conservée + alternative native : prose adaptée selon surface ; conf MCP injectée par CLI au LaunchAgent dans le run dir isolé (McpConfigStrategy : ConfigFile/Flag/Env), symétrique au convention file et au seed permissions.
  4. Frontières : serveur MCP = adapter entrant infra (infrastructure/src/orchestrator/mcp/), pair du watcher fichier, appelant le même OrchestratorService::dispatch. Aucun nouveau port applicatif/domaine.
  5. Lots : M0 (capacité profil) → M1 (injection conf) → M2 (adapter/serveur MCP, spike confiné) → M3 (câblage par projet) → M4 (observabilité, optionnel).