UNPKG

overmind-mcp

Version:

Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour

221 lines • 10.8 kB
import { runAgent } from '../../tools/run_agent.js'; // ─── Helpers ────────────────────────────────────────────────────────────────── // eslint-disable-next-line @typescript-eslint/no-explicit-any function extractResultText(result) { if (Array.isArray(result?.content)) { return result.content .filter((c) => c.type === 'text') .map((c) => c.text) .join('\n'); } return String(result ?? ''); } // ─── Core logic ──────────────────────────────────────────────────────────────── export async function runAgentsLocally(agents, opts) { const { waitAll } = opts; const startTime = Date.now(); const controllers = agents.map(() => new AbortController()); const settled = new Array(agents.length).fill(false); const promises = agents.map(async (agentArgs, index) => { const label = agentArgs.taskId || agentArgs.agentName || `task_${index + 1}`; const taskStart = Date.now(); try { const result = await runAgent({ ...agentArgs, signal: controllers[index].signal }); settled[index] = true; const elapsed = ((Date.now() - taskStart) / 1000).toFixed(1); const text = extractResultText(result); return { label, runner: agentArgs.runner, agentName: agentArgs.agentName, status: result?.isError ? 'error' : 'success', elapsed: `${elapsed}s`, result: text, }; } catch (err) { settled[index] = true; const elapsed = ((Date.now() - taskStart) / 1000).toFixed(1); const msg = err instanceof Error ? err.message : String(err); return { label, runner: agentArgs.runner, agentName: agentArgs.agentName, status: 'error', elapsed: `${elapsed}s`, result: msg, }; } }); if (waitAll) { const settledResults = await Promise.allSettled(promises); const results = settledResults.map((s, i) => { const label = agents[i].taskId || agents[i].agentName || `task_${i + 1}`; if (s.status === 'fulfilled') return s.value; return { label, runner: agents[i].runner, agentName: agents[i].agentName, status: 'error', elapsed: '?', result: s.reason instanceof Error ? s.reason.message : String(s.reason), }; }); const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1); const successCount = results.filter((r) => r.status === 'success').length; const errorCount = results.filter((r) => r.status === 'error').length; const summary = [ `⚡ run_agents_parallel — ${results.length} agent(s) | ✅ ${successCount} succès | ❌ ${errorCount} erreurs | 🕐 ${totalElapsed}s total`, '', ...results.map((r) => { const icon = r.status === 'success' ? '✅' : '❌'; const header = `${icon} [${r.label}] ${r.runner}${r.agentName ? `/${r.agentName}` : ''} (${r.elapsed})`; return `${header}\n${r.result}`; }), ].join('\n---\n'); return { content: [{ type: 'text', text: summary }], isError: errorCount === results.length, }; } else { // Race mode: get first result, abort others, AND catch unhandled rejections const firstResult = await Promise.race(promises); // Abort all unsettled agents for (let i = 0; i < controllers.length; i++) { if (!settled[i]) { controllers[i].abort(); } } // Catch any remaining promises to prevent unhandled rejections // (they may reject after being aborted) for (let i = 0; i < promises.length; i++) { if (!settled[i]) { promises[i].catch(() => { }); // Silently absorb abort rejections } } const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1); const singleResult = [firstResult]; const successCount = singleResult.filter((r) => r.status === 'success').length; const errorCount = singleResult.filter((r) => r.status === 'error').length; const summary = [ `⚡ run_agents_parallel — ${singleResult.length} agent(s) | ✅ ${successCount} succès | ❌ ${errorCount} erreurs | 🕐 ${totalElapsed}s total`, '', ...singleResult.map((r) => { const icon = r.status === 'success' ? '✅' : '❌'; const header = `${icon} [${r.label}] ${r.runner}${r.agentName ? `/${r.agentName}` : ''} (${r.elapsed})`; return `${header}\n${r.result}`; }), ].join('\n---\n'); return { content: [{ type: 'text', text: summary }], isError: errorCount === singleResult.length, }; } } // ─── Dispatcher ──────────────────────────────────────────────────────────────── /** * Main dispatcher entry point (v3.0 — Refactored). * * Routing logic: * - ALL agents are Hermes + waitAll=true → dispatch via Kanban (durable, SQLite-backed) * - Otherwise → dispatch via runAgentsLocally (in-process, spawn direct) * * Kanban dispatch is only used for Hermes-only groups because: * 1. Kanban provides durability (survives crashes) * 2. Kanban provides circuit breaker (auto-reclaim, auto-retry) * 3. Kanban provides dependency graph (parent→child) * 4. Non-Hermes runners (claude, kilo, etc.) can't be spawned by the Kanban dispatcher */ export async function dispatchAgents(agents, opts) { const allHermes = agents.length > 1 && agents.every((a) => a.runner === 'hermes'); // ─── All-Hermes parallel dispatch → Kanban (durable) ────────────────────── if (allHermes && opts.waitAll) { try { return await dispatchViaKanban(agents, opts); } catch (e) { // Fallback to in-process dispatch if kanban fails const msg = e instanceof Error ? e.message : String(e); console.error(`[dispatchAgents] Kanban dispatch failed (${msg}) — falling back to in-process.`); } } // ─── Default: in-process dispatch ───────────────────────────────────────── return runAgentsLocally(agents, opts); } /** * Dispatch a group of Hermes agents via Kanban tasks. * * Each agent becomes a Kanban task assigned to its profile. The Kanban dispatcher * spawns each as a separate OS process with circuit breaker, retry, and durability. * * YOLO: auto-reclaim on stale, auto-unblock on block, no human intervention. */ async function dispatchViaKanban(agents, _opts) { const { KanbanAdapter } = await import('../../services/KanbanAdapter.js'); const kanban = new KanbanAdapter(); const startTime = Date.now(); // ─── Ensure kanban is initialized ───────────────────────────────────────── try { await kanban.init(); } catch { // init is idempotent — ignore errors } // ─── Create parallel tasks ──────────────────────────────────────────────── const taskDefs = agents.map((agent, i) => ({ title: agent.taskId || agent.agentName || `task_${i + 1}`, assignee: agent.agentName || 'default', body: agent.prompt, idempotencyKey: agent.taskId, // dedup if taskId is provided })); const taskIds = await kanban.createParallelTasks(taskDefs); // ─── Wait for all tasks to complete ─────────────────────────────────────── const timeoutMs = 600000; // 10min per task const results = await Promise.all(taskIds.map(async (taskId, i) => { const label = agents[i].taskId || agents[i].agentName || `task_${i + 1}`; const taskStart = Date.now(); try { const result = await kanban.wait(taskId, timeoutMs); const elapsed = ((Date.now() - taskStart) / 1000).toFixed(1); return { label, runner: 'hermes', agentName: agents[i].agentName, status: (result.status === 'done' ? 'success' : 'error'), elapsed: `${elapsed}s`, result: result.summary || result.error || '(no output)', }; } catch (e) { const elapsed = ((Date.now() - taskStart) / 1000).toFixed(1); return { label, runner: 'hermes', agentName: agents[i].agentName, status: 'error', elapsed: `${elapsed}s`, result: e instanceof Error ? e.message : String(e), }; } })); // ─── Build summary ──────────────────────────────────────────────────────── const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1); const successCount = results.filter((r) => r.status === 'success').length; const errorCount = results.filter((r) => r.status === 'error').length; const summary = [ `⚡ dispatch_via_kanban — ${results.length} agent(s) | ✅ ${successCount} succès | ❌ ${errorCount} erreurs | 🕐 ${totalElapsed}s total`, '', ...results.map((r) => { const icon = r.status === 'success' ? '✅' : '❌'; const header = `${icon} [${r.label}] ${r.runner}${r.agentName ? `/${r.agentName}` : ''} (${r.elapsed})`; return `${header}\n${r.result}`; }), ].join('\n---\n'); return { content: [{ type: 'text', text: summary }], isError: errorCount === results.length, }; } //# sourceMappingURL=dispatcher.js.map