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

271 lines • 10.9 kB
import { z } from 'zod'; import { getProcessStatus, killAgent } from '../lib/processRegistry.js'; import { getLiveAgent, getLiveAgentByPid, } from '../lib/agent_lifecycle.js'; // ─── Schema + Types ────────────────────────────────────────────────────────── export const agentControlSchema = z.object({ agentName: z.string().describe("Nom unique de l'agent à contrôler"), runner: z .enum(['claude', 'gemini', 'kilo', 'qwencli', 'openclaw', 'cline', 'opencode', 'hermes']) .optional() .describe('Type de runner (optionnel — déduit si omis)'), config: z.string().optional().describe('Chemin racine Overmind'), action: z .enum(['status', 'stream', 'kill', 'wait']) .describe('status — état courant (pid, status, sessionId, output)\n' + 'stream — output en temps réel + isComplete\n' + 'kill — arrêt forcé du process tree\n' + 'wait — bloque jusquà terminaison (max timeoutMs)'), timeoutMs: z .number() .int() .min(1000) .max(3600000) .optional() .default(900000) .describe('Timeout wait en ms (défaut: 900s, max: 1h)'), sinceTimestamp: z.number().optional().describe('Pour stream: output après ce timestamp'), }); /** Try in-memory first (fast), fall back to disk registry */ async function resolveAgent(agentName, runner) { const live = getLiveAgent(agentName, runner); if (live) return { live, disk: null }; const disk = await getProcessStatus(agentName, runner); return { live: null, disk }; } // ─── Formatting ─────────────────────────────────────────────────────────────── const OUTPUT_TAIL_CHARS = 2000; function formatAgent(view, _action, outputTail = OUTPUT_TAIL_CHARS) { const lines = []; const runningFor = view.startedAt ? `${Date.now() - view.startedAt}ms` : '?'; lines.push(`**Agent:** ${view.agentName}`); lines.push(`**Runner:** ${view.runner || 'inconnu'}`); lines.push(`**Status:** ${view.status}`); lines.push(`**Running:** ${runningFor}`); if (view.pid) lines.push(`**PID:** ${view.pid}`); if (view.sessionId) lines.push(`**Session:** ${view.sessionId}`); if (view.exitCode !== null) lines.push(`**Exit Code:** ${view.exitCode}`); if (view.lastOutputAt) { lines.push(`**Last Output:** ${new Date(view.lastOutputAt).toISOString()}`); } const bufLen = view.outputBuffer.length; if (bufLen > 0) { lines.push(`\n**Output (${bufLen} chars, last ${outputTail}):**`); lines.push('```'); lines.push(view.outputBuffer.slice(-outputTail)); lines.push('```'); } else { lines.push('\n_(output vide)_'); } return lines.join('\n'); } // ─── Actions ─────────────────────────────────────────────────────────────────── /** STATUS — zero side-effect read */ async function doStatus(agentName, runner) { const { live, disk } = await resolveAgent(agentName, runner); if (!live && !disk) { return { content: [{ type: 'text', text: `Agent "${agentName}" non trouvé.` }], isError: true, }; } const view = live ? { agentName: live.agentName, runner: live.runner, status: live.status, pid: live.pid, sessionId: live.sessionId, exitCode: live.exitCode, outputBuffer: live.outputBuffer, startedAt: live.startedAt, lastOutputAt: live.lastOutputAt, } : { agentName: disk.agentName, runner: disk.runner, status: disk.status, pid: disk.pid, sessionId: disk.id, exitCode: disk.exitCode ?? null, outputBuffer: '', // disk entry has no outputBuffer startedAt: disk.ts, lastOutputAt: disk.lastOutputAt ?? disk.ts, }; const isZombie = view.status === 'running' && view.pid && !getLiveAgentByPid(view.pid); return { content: [{ type: 'text', text: formatAgent(view, 'status') }], isError: !!isZombie, }; } /** STREAM — read + completeness flag */ async function doStream(agentName, runner, sinceTimestamp) { const { live, disk } = await resolveAgent(agentName, runner); if (!live && !disk) { return { content: [{ type: 'text', text: `Agent "${agentName}" non trouvé.` }], isError: true, }; } const isComplete = live ? live.status !== 'running' : disk.status !== 'running'; const output = live ? live.outputBuffer : ''; if (sinceTimestamp && live) { // Ring buffer doesn't support per-chunk timestamps — best-effort: return last 2000 void sinceTimestamp; } const view = live ? { agentName: live.agentName, runner: live.runner, status: live.status, pid: live.pid, sessionId: live.sessionId, exitCode: live.exitCode, outputBuffer: output, startedAt: live.startedAt, lastOutputAt: live.lastOutputAt, } : { agentName: disk.agentName, runner: disk.runner, status: disk.status, pid: disk.pid, sessionId: disk.id, exitCode: disk.exitCode ?? null, outputBuffer: '', startedAt: disk.ts, lastOutputAt: disk.lastOutputAt ?? disk.ts, }; const lines = []; lines.push(`**Agent:** ${view.agentName}`); lines.push(`**Status:** ${view.status}`); lines.push(`**isComplete:** ${isComplete}`); if (view.pid) lines.push(`**PID:** ${view.pid}`); if (view.lastOutputAt) lines.push(`**Last Output At:** ${new Date(view.lastOutputAt).toISOString()}`); lines.push(`\n**Output (${output.length} chars):**`); lines.push('```'); lines.push(output.slice(-2000) || '(no output yet)'); lines.push('```'); return { content: [{ type: 'text', text: lines.join('\n') }], isError: isComplete && view.status === 'failed', }; } /** KILL — force termination */ async function doKill(agentName, runner) { const { live } = await resolveAgent(agentName, runner); if (!live || live.status !== 'running') { const disk = await getProcessStatus(agentName, runner); return { content: [ { type: 'text', text: disk ? `Agent "${agentName}" nest plus en cours (status: ${disk.status}).` : `Agent "${agentName}" non trouvé.`, }, ], isError: true, }; } const pid = live.pid; const result = await killAgent(agentName, runner); return { content: [ { type: 'text', text: result.killed ? `Agent "${agentName}" tué (PID: ${pid}).` : `Échec du kill pour "${agentName}".`, }, ], isError: !result.killed, }; } /** WAIT — block until completion, using AbortController when available */ async function doWait(agentName, runner, timeoutMs) { const live = getLiveAgent(agentName, runner); // Fast path: agent already done if (live) { if (live.status !== 'running') { return { content: [{ type: 'text', text: live.outputBuffer || `Agent terminé (${live.status}).` }], isError: live.status === 'failed' || live.status === 'orphaned', }; } // Use AbortController if available (instant wake on kill/status change) if (live.abortController) { try { await Promise.race([ new Promise((_, reject) => { live.abortController.signal.addEventListener('abort', () => reject(new Error('done'))); }), new Promise((r) => setTimeout(r, timeoutMs)), ]); } catch { // Aborted — agent finished } // Re-read state after wake const updated = getLiveAgent(agentName, runner); if (updated) { return { content: [ { type: 'text', text: updated.outputBuffer || `Agent terminé (${updated.status}).` }, ], isError: updated.status === 'failed' || updated.status === 'orphaned', }; } } } else { // Fallback: disk polling const disk = await getProcessStatus(agentName, runner); if (!disk || disk.status !== 'running') { return { content: [ { type: 'text', text: disk ? `Agent terminé (${disk.status}).` : `Agent non trouvé.` }, ], isError: true, }; } } // Polling fallback (disk or no AbortController) const start = Date.now(); const pollInterval = 1000; while (Date.now() - start < timeoutMs) { await new Promise((r) => setTimeout(r, pollInterval)); const current = getLiveAgent(agentName, runner); if (!current || current.status !== 'running') { return { content: [{ type: 'text', text: current?.outputBuffer || 'Agent terminé.' }], isError: current?.status === 'failed' || current?.status === 'orphaned' || !current, }; } } return { content: [{ type: 'text', text: `Timeout ${timeoutMs}ms atteint. Agent toujours en cours.` }], isError: true, }; } // ─── Main entry point ───────────────────────────────────────────────────────── export async function agentControl(args) { const { agentName, runner, action, timeoutMs, sinceTimestamp } = args; switch (action) { case 'status': return doStatus(agentName, runner); case 'stream': return doStream(agentName, runner, sinceTimestamp); case 'kill': return doKill(agentName, runner); case 'wait': return doWait(agentName, runner, timeoutMs ?? 900000); } } //# sourceMappingURL=agent_control.js.map