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

@ -30,6 +30,13 @@ export interface AgentsViewModel {
profiles: AgentProfile[];
/** Agents that currently own a live PTY session, as tracked by IdeA. */
liveAgents: LiveAgent[];
/**
* The entry door (`"mcp"` | `"file"`) the *last* orchestration delegation from
* each requester agent arrived through, keyed by requester id. Populated from
* `orchestratorRequestProcessed` events; a requester absent here (or an event
* without `source`) yields no badge.
*/
delegationSourceByRequester: Record<string, "mcp" | "file">;
/** Last error message, or `null`. */
error: string | null;
/** Whether a request is in flight. */
@ -96,6 +103,9 @@ export function useAgents(projectId: string): AgentsViewModel {
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [runningAgentId, setRunningAgentId] = useState<string | null>(null);
const [delegationSourceByRequester, setDelegationSourceByRequester] = useState<
Record<string, "mcp" | "file">
>({});
const refresh = useCallback(async () => {
setBusy(true);
@ -150,6 +160,19 @@ export function useAgents(projectId: string): AgentsViewModel {
void refresh();
void refreshLiveAgents();
}
// Record which door a delegation came through (mcp vs file). Absent
// `source` (older backend) leaves the map untouched → no badge.
if (
event.type === "orchestratorRequestProcessed" &&
event.source !== undefined
) {
const { requesterId, source } = event;
setDelegationSourceByRequester((prev) =>
prev[requesterId] === source
? prev
: { ...prev, [requesterId]: source },
);
}
})
.then((un) => {
if (cancelled) un();
@ -317,6 +340,7 @@ export function useAgents(projectId: string): AgentsViewModel {
context,
profiles,
liveAgents,
delegationSourceByRequester,
error,
busy,
runningAgentId,