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
656 lines (507 loc) • 21 kB
Markdown
# `agent_control` — Contrôle Unifié du Cycle de Vie des Agents
> **Outil unifié** qui remplace les 4 outils précédents : `get_agent_status`, `stream_agent_output`, `kill_agent`, `wait_agent`.
> Document technique complet : usage, patterns async, lookup par PID/timestamp, dashboard.
## Table des Matières
1. [Vue d'Ensemble](#1-vue-densemble)
2. [Actions Disponibles](#2-actions-disponibles)
3. [Codes d'Erreur](#3-codes-derreur)
4. [Patterns Async avec OverMind](#4-patterns-async-avec-overmind)
5. [Tracker PID ↔ Session ↔ Agent](#5-tracker-pid--session--agent)
6. [Dashboard en Temps Réel](#6-dashboard-en-temps-réel)
7. [Flux Complet de Debug](#7-flux-complet-de-debug)
8. [Référence Rapide](#8-référence-rapide)
## 1. Vue d'Ensemble
### Problème Résolu
Quand un agent est lancé via `run_agent`, le processus fils (`claude`, `kilo`, etc.) tourne en arrière-plan. Le seul lien entre le processus parent (OverMind MCP) et le processus enfant est le `sessionId` — généré par le runner, opaque pour OverMind.
**Problème** : Si OverMind restart ou crash, le `sessionId` est perdu et le child process devient **orphelin**.
**Solution** : Le **Process Registry** (`sessions.json`) stocke le mapping complet :
```
pid ↔ sessionId ↔ agentName ↔ runner ↔ status ↔ outputBuffer
```
`agent_control` est l'unique interface pour interagir avec ce registry.
### Architecture
```
Client OverMind MCP Process
│ │ │
│ run_agent() │ spawn(claude) │
│──────────────────────────────────►│ registerProcess(pid) │
│ │─────────────────────────────►│
│ │ │
│ agent_control(action: "status") │ │
│──────────────────────────────────►│ getProcessStatus(pid) │
│ { status, pid, outputBuffer } │ │
│◄─────────────────────────────────│ │
│ │ │
│ agent_control(action: "stream") │ │
│──────────────────────────────────►│ read outputBuffer │
│ { output, isComplete } │ │
│◄─────────────────────────────────│ │
│ │ │
│ agent_control(action: "wait") │ poll every 1s... │
│──────────────────────────────────►│ │
│ │ child.on('close') │
│ { result } │◄─────────────────────────────│
│◄─────────────────────────────────│ │
```
## 2. Actions Disponibles
### `status` — Lecture Pure, Zero Side-Effect
Retourne l'état courant du process **sans modifier le registry**.
```javascript
agent_control({
agentName: 'sniper_analyst',
runner: 'kilo',
action: 'status',
})
```
**Réponse :**
```markdown
**Agent:** sniper_analyst
**Runner:** kilo
**Status:** running
**Started:** 2026-05-10T14:32:00.000Z
**PID:** 12345
**Session ID:** sess_abc123
**Output Buffer (2048 chars):**
```
Thinking...
Fetching BTC data...
Analysis complete.
```
```
**États possibles :**
| Status | Signification |
|--------|---------------|
| `running` | Process actif, PID valide |
| `done` | Terminé avec code 0 |
| `failed` | Terminé avec erreur (exit code != 0) |
| `orphaned` | Parent mort mais child tourne encore |
### `stream` — Lecture + Indicateur de Complétude
Retourne l'output accumulé + un flag `isComplete` pour savoir si le process est fini.
```javascript
agent_control({
agentName: 'sniper_analyst',
runner: 'kilo',
action: 'stream',
sinceTimestamp: 1746892800000, // optionnel
})
```
**Réponse :**
```markdown
**Agent:** sniper_analyst
**Status:** running
**isComplete:** false
**PID:** 12345
**Last Output At:** 2026-05-10T14:32:45.000Z
**Output (2048 chars):**
```
Thinking...
Fetching BTC data...
```
```
Quand `isComplete: true` :
```markdown
**Agent:** sniper_analyst
**Status:** done
**isComplete:** true
**PID:** 12345
**Output (4096 chars):**
```
Final analysis: BUY signal detected.
```
```
### `kill` — Destruction Irréversible
Tue le process tree via `taskkill /F /T /PID` (Windows) ou `kill -9` (Unix).
```javascript
agent_control({
agentName: 'sniper_analyst',
runner: 'kilo',
action: 'kill',
})
```
**Réponse (succès) :**
```markdown
Agent "sniper_analyst" tué avec succès (PID: 12345). Status mis à jour → 'failed' dans le registry.
```
**Réponse (échec) :**
```markdown
Agent "sniper_analyst" n'est pas en cours d'exécution (status: done). Impossible de tuer un agent déjà terminé.
```
> **⚠️ IRRÉVERSIBLE** : Une fois tué, le process ne peut pas être récupéré. Utiliser `kill` uniquement pour un abort d'urgence ou kill-switch.
### `wait` — Blocage Async avec Polling
Poll toutes les 1s jusqu'à ce que le status ne soit plus `running`, ou que le timeout soit atteint.
```javascript
agent_control({
agentName: 'sniper_analyst',
runner: 'kilo',
action: 'wait',
timeoutMs: 300000, // 5 minutes (défaut: 900000 = 15 min)
})
```
**Réponse (terminé) :**
```markdown
Final analysis: BUY signal detected. Portfolio balanced.
```
**Réponse (timeout) :**
```markdown
Timeout de 300000ms atteint. L'agent "sniper_analyst" est toujours en cours d'exécution (status: running). Utilisez action="kill" pour forcer l'arrêt ou augmentez timeoutMs.
```
**Réponse (erreur) :**
```markdown
Agent terminé avec erreur (failed):
TypeError: Cannot read property 'price' of undefined
at analyzeBTC (/app/bot.js:42)
at processTicksAndCallbacks (internal/process/task_queues.js:95)
```
## 3. Codes d'Erreur
Chaque erreur est structurée avec un **code** pour faciliter le debugging programatique.
| Code | Signification | Action recommandée |
|------|---------------|---------------------|
| `AGENT_NOT_FOUND` | Agent absent du registry | Vérifier le nom ou le runner |
| `AGENT_NOT_RUNNING` | Action `kill` sur un agent déjà terminé | Ne pas tuer un agent already done |
| `KILL_FAILED` | `taskkill`/`kill` a échoué | Vérifier permissions Windows/Unix |
| `WAIT_TIMEOUT` | Timeout atteint sans terminaison | Augmenter `timeoutMs` ou utiliser `kill` |
| `ORPHANED_PROCESS` | Process zombie détecté | `kill` puis relancer l'agent |
## 4. Patterns Async avec OverMind
### Pattern 1 : Lancer et Ne Pas Bloquer (Fire & Forget)
```javascript
// 1. Lancer l'agent en arrière-plan
const runResult = await run_agent({
runner: 'kilo',
agentName: 'crypto_scanner',
prompt: 'Scan BTC/USDT for trading opportunities',
});
// 2. Récupérer le PID depuis le output (format: "PID: 12345")
// OU depuis le sessionId pour retrouver le process plus tard
const sessionId = runResult.sessionId; // "sess_abc123"
// 3. Polling non-bloquant pour vérifier l'état
const status = await agent_control({
agentName: 'crypto_scanner',
runner: 'kilo',
action: 'status',
});
```
### Pattern 2 : Lancer et Attendre (Blocking Wait)
```javascript
// Une seule ligne pour lancer et attendre
const result = await agent_control({
agentName: 'long_task',
runner: 'claude',
action: 'wait',
timeoutMs: 600000, // 10 minutes
});
if (result.isError) {
console.error('Task failed:', result.content[0].text);
} else {
console.log('Result:', result.content[0].text);
}
```
### Pattern 3 : Orchestration Séquentielle
```javascript
async function runWorkflow(steps) {
const results = [];
for (const step of steps) {
console.log(`Starting step: ${step.name}`);
const result = await agent_control({
agentName: step.agentName,
runner: step.runner,
action: 'wait',
timeoutMs: step.timeoutMs || 300000,
});
if (result.isError) {
return {
success: false,
failedStep: step.name,
error: result.content[0].text,
partialResults: results,
};
}
results.push({ step: step.name, output: result.content[0].text });
}
return { success: true, results };
}
// Usage
const workflow = await runWorkflow([
{ name: 'fetch_data', agentName: 'data_fetcher', runner: 'kilo', timeoutMs: 60000 },
{ name: 'analyze', agentName: 'analyzer', runner: 'claude', timeoutMs: 300000 },
{ name: 'report', agentName: 'reporter', runner: 'kilo', timeoutMs: 120000 },
]);
```
### Pattern 4 : Exécution Parallèle (Fan-Out)
```javascript
// Lancer plusieurs agents en parallèle
const agents = [
{ agentName: 'btc_analyst', runner: 'kilo', prompt: 'Analyze BTC' },
{ agentName: 'eth_analyst', runner: 'kilo', prompt: 'Analyze ETH' },
{ agentName: 'sol_analyst', runner: 'claude', prompt: 'Analyze SOL' },
];
// Lancer les 3 en parallèle
const runPromises = agents.map(a =>
run_agent({ runner: a.runner, agentName: a.agentName, prompt: a.prompt })
);
await Promise.all(runPromises);
// Attendre que tous soient prêts
const waitPromises = agents.map(a =>
agent_control({ agentName: a.agentName, runner: a.runner, action: 'wait', timeoutMs: 300000 })
);
const results = await Promise.all(waitPromises);
for (const [i, r] of results.entries()) {
console.log(`${agents[i].agentName}:`, r.isError ? 'FAILED' : 'OK');
}
```
### Pattern 5 : Resume après Crash OverMind
Quand OverMind restart, on peut se rattacher aux agents en cours via le registry :
```javascript
// 1. Scanner tous les agents "running" dans le registry
const runningAgents = await getRunningProcesses();
// 2. Pour chaque agent encore vivant mais "orphaned"
for (const agent of runningAgents) {
const status = await agent_control({
agentName: agent.agentName,
runner: agent.runner,
action: 'status',
});
if (status.content[0].text.includes('orphaned')) {
console.log(`Orphaned agent detected: ${agent.agentName} (PID: ${agent.pid})`);
// Option: kill et relancer, ou laisser tel quel
}
}
```
## 5. Tracker PID ↔ Session ↔ Agent
Le registry dans `sessions.json` stocke la cartographie complète :
```json
{
"kilo:sniper_analyst": {
"id": "sess_abc123",
"ts": 1746892800000,
"pid": 12345,
"runner": "kilo",
"agentName": "sniper_analyst",
"status": "running",
"outputBuffer": "Thinking...\nFetching data...",
"exitCode": null,
"lastOutputAt": 1746892900000
},
"claude:reporter": {
"id": "sess_def456",
"ts": 1746892850000,
"pid": 67890,
"runner": "claude",
"agentName": "reporter",
"status": "done",
"outputBuffer": "Report generated successfully.",
"exitCode": 0,
"lastOutputAt": 1746893500000
}
}
```
### Trouver un Agent par Timestamp
```javascript
// Trouver tous les agents活跃 après une date donnée
async function findAgentsAfter(timestampMs) {
const allAgents = await getProcessStatus('*'); // sans runner = tous
return allAgents
.filter(a => a.ts >= timestampMs)
.sort((a, b) => a.ts - b.ts); // plus récents d'abord
}
// Usage
const recent = await findAgentsAfter(Date.now() - 3600000); // dernière heure
console.log(recent.map(a => `${a.agentName} @ ${new Date(a.ts).toISOString()}`));
```
### Trouver un Agent par PID
```javascript
// Via le registry scan
async function findAgentByPid(pid) {
const { store } = await readStore();
for (const [key, entry] of Object.entries(store)) {
if (typeof entry === 'object' && entry !== null && entry.pid === pid) {
return { key, ...entry };
}
}
return null;
}
// Usage
const agent = await findAgentByPid(12345);
console.log(`Found: ${agent.agentName} (${agent.runner})`);
```
### Trouver un Agent par Session ID
```javascript
// Via le registry scan
async function findAgentBySession(sessionId) {
const { store } = await readStore();
for (const [key, entry] of Object.entries(store)) {
if (typeof entry === 'object' && entry !== null && entry.id === sessionId) {
return { key, ...entry };
}
}
return null;
}
// Usage
const agent = await findAgentBySession('sess_abc123');
if (agent) {
console.log(`PID: ${agent.pid}, Status: ${agent.status}`);
}
```
## 6. Dashboard en Temps Réel
### Exemple Complet : Dashboard CLI
```javascript
import { getRunningProcesses } from './lib/processRegistry.js';
async function dashboard() {
console.clear();
console.log('═══════════════════════════════════════════════════════');
console.log(' OVERMIND AGENT DASHBOARD');
console.log('═══════════════════════════════════════════════════════\n');
const running = await getRunningProcesses();
if (running.length === 0) {
console.log('Aucun agent en cours d\'exécution.\n');
return;
}
console.log(`📊 ${running.length} agent(s) actifs\n`);
console.log('┌──────────────────┬────────┬─────────┬───────────────┬────────────────────────┐');
console.log('│ Agent │ Runner │ PID │ Status │ Started │');
console.log('├──────────────────┼────────┼─────────┼───────────────┼────────────────────────┤');
for (const agent of running) {
const elapsed = Date.now() - agent.ts;
const elapsedStr = elapsed < 60000
? `${Math.floor(elapsed / 1000)}s`
: `${Math.floor(elapsed / 60000)}m`;
const statusColor = agent.status === 'running' ? '🟢' : '⚠️';
console.log(
`│ ${agent.agentName.padEnd(16)} │ ${(agent.runner || '?').padEnd(6)} │ ${String(agent.pid || 'N/A').padStart(7)} │ ${statusColor} ${agent.status.padEnd(11)} │ ${new Date(agent.ts).toLocaleTimeString().padEnd(22)} │`
);
}
console.log('└──────────────────┴────────┴─────────┴───────────────┴────────────────────────┘\n');
// Detail du premier agent
if (running[0]) {
const detail = await agent_control({
agentName: running[0].agentName,
runner: running[0].runner,
action: 'stream',
});
console.log('═══ OUTPUT RECENT ═══');
console.log(detail.content[0].text.slice(-500));
}
}
// Rafraîchir toutes les 3 secondes
setInterval(dashboard, 3000);
```
### Exemple : Tableau de Bord HTML
```html
<!DOCTYPE html>
<html>
<head>
<title>OverMind Dashboard</title>
<meta http-equiv="refresh" content="5">
<style>
body { font-family: monospace; background: #0d1117; color: #e6edf3; padding: 20px; }
.agent { border: 1px solid #30363d; padding: 10px; margin: 10px 0; border-radius: 6px; }
.running { border-left: 4px solid #3fb950; }
.done { border-left: 4px solid #58a6ff; }
.failed { border-left: 4px solid #f85149; }
.orphaned { border-left: 4px solid #d29922; }
.pid { color: #8b949e; }
.output { background: #161b22; padding: 8px; margin-top: 8px; max-height: 100px; overflow: auto; }
</style>
</head>
<body>
<h1>🤖 OverMind Agent Dashboard</h1>
<div id="agents"></div>
<script>
async function refresh() {
// Appeler agent_control pour chaque agent running
const response = await fetch('/api/agents/running');
const agents = await response.json();
document.getElementById('agents').innerHTML = agents.map(agent => `
<div class="agent ${agent.status}">
<strong>${agent.agentName}</strong>
<span class="pid">(${agent.runner} | PID: ${agent.pid})</span>
<br>
Status: ${agent.status} | Started: ${new Date(agent.ts).toLocaleString()}
<br>
Elapsed: ${Math.floor((Date.now() - agent.ts) / 1000)}s
<div class="output">${escapeHtml(agent.outputBuffer || '(no output)').slice(-200)}</div>
</div>
`).join('');
}
function escapeHtml(text) {
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
```
## 7. Flux Complet de Debug
Quand un agent ne répond plus ou pose problème :
```
STEP 1: Identifier le problème
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
agent_control({ agentName: "sniper_analyst", action: "status" })
→ Si status: "running" + lastOutputAt ancien → agent peut-être bloqué
→ Si status: "orphaned" → parent mort, child tourne encore
→ Si status: "done" → agent déjà terminé, problem elsewhere
STEP 2: Voir l'output
━━━━━━━━━━━━━━━━━━━━━━
agent_control({ agentName: "sniper_analyst", action: "stream" })
→ Identifier la dernière ligne avant le blocage
→ Vérifier s'il y a des erreurs dans le buffer
STEP 3: Décider de l'action
━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ still running, output OK → Attendre (action: "wait", timeoutMs: X)
│
├─ still running, BLOCKED → kill (action: "kill") → restart
│
├─ orphaned → kill (action: "kill") → restart
│
└─ done with error → Analyser output → fix prompt → restart
STEP 4: Nettoyer si nécessaire
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Le registry fait le cleanup automatique après 1h (TTL).
Pour forcer le cleanup immédiat :
unregisterProcess(pid) → supprime l'entrée du registry
```
## 8. Référence Rapide
```javascript
// Status simple
agent_control({ agentName: "...", action: "status" })
// Stream avec filtre timestamp
agent_control({ agentName: "...", action: "stream", sinceTimestamp: 1746892800000 })
// Kill
agent_control({ agentName: "...", action: "kill" })
// Wait avec timeout custom
agent_control({ agentName: "...", action: "wait", timeoutMs: 300000 })
// Avec runner explicite (recommendé)
agent_control({ agentName: "...", runner: "kilo", action: "..." })
// Avec config path (si plusieurs OverMind)
agent_control({ agentName: "...", config: "/path/to/overmind", action: "..." })
```
### Paramètres Communs
| Param | Type | Description |
|-------|------|-------------|
| `agentName` | string | Nom unique de l'agent |
| `runner` | enum | Type de runner (optionnel, déduit si omis) |
| `config` | string | Chemin racine Overmind (optionnel) |
### Actions
| Action | Description | Retourne |
|--------|-------------|----------|
| `status` | État courant | pid, status, sessionId, outputBuffer |
| `stream` | Output + complétude | output, isComplete |
| `kill` | Arrêt forcé | confirmation ou erreur |
| `wait` | Attente terminaison | output final ou timeout |
**Fichier source** : `src/tools/agent_control.ts`
**Registry** : `src/lib/processRegistry.ts`
**Store** : `bridge/agents.json`
**TTL** : 1h après terminaison (cleanup automatique)