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>
This commit is contained in:
2026-06-10 12:53:31 +02:00
parent 97daf3fae5
commit 37e72747d3
30 changed files with 3646 additions and 27 deletions

View File

@ -147,6 +147,7 @@ impl OrchestratorService {
OrchestratorCommand::AskAgent { target, task } => {
self.ask_agent(project, target, task).await
}
OrchestratorCommand::ListAgents => self.list_agents(project).await,
OrchestratorCommand::StopAgent { name } => self.stop_agent(project, name).await,
OrchestratorCommand::UpdateAgentContext { name, context } => {
self.update_agent_context(project, name, context).await
@ -355,6 +356,35 @@ impl OrchestratorService {
}
}
/// `list_agents`: discovery — return the project's agents exactly as the UI
/// reads them from the manifest, via the **same** [`ListAgents`] use case.
///
/// The list is serialised as a JSON array into [`OrchestratorOutcome::reply`]
/// (the existing inline-payload channel, also used by `ask`), with a one-line
/// count in [`OrchestratorOutcome::detail`]. Each element carries the agent's
/// `id`, `name`, `contextPath`, `profileId`, `origin`, `synchronized` and
/// `skills` (camelCase, the [`domain::Agent`] serde shape).
///
/// # Errors
/// Propagates [`AppError`] from the use case (manifest load / invariant) or a
/// serialisation failure ([`AppError::Invalid`]).
async fn list_agents(&self, project: &Project) -> Result<OrchestratorOutcome, AppError> {
let listed = self
.list_agents
.execute(ListAgentsInput {
project: project.clone(),
})
.await?;
let reply = serde_json::to_string(&listed.agents)
.map_err(|e| AppError::Invalid(format!("failed to serialise agent list: {e}")))?;
Ok(OrchestratorOutcome {
detail: format!("listed {} agent(s)", listed.agents.len()),
reply: Some(reply),
})
}
/// `stop_agent`: translate the agent name → its live session → `CloseTerminal`.
async fn stop_agent(
&self,