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
766 lines • 37.9 kB
JavaScript
/**
* ═══════════════════════════════════════════════════════════════════════════
* A2A Hub — Outil MCP pour la communication Agent-to-Agent
* ═══════════════════════════════════════════════════════════════════════════
*
* Architecture distribuée — chaque bridge est un serveur HTTP indépendant:
*
* a2a_hub parle DIRECTEMENT aux bridges HTTP:
* POST :31XX/rpc → JSON-RPC 2.0 agent.run (NEXUS bridges)
* POST :30XX/send → legacy message synchrone (discord_llm bridge)
* GET :XXXX/health → status live
*
* Découverte automatique:
* 1. Scan ports 3101-3120 (NEXUS bridges) + 3001-3020 (legacy workers)
* 2. Scan ~/.overmind/hermes/profiles/ (Linux) + ~/AppData/Local/hermes/profiles/ (Windows)
* 3. Cross-reference: profile ↔ bridge port
* 4. Lit Nexus/tmp/bridge_pids.json si présent
*
* PROD FIXES (2026-07-10):
* - fetch() async au lieu de execSync('curl') — ne bloque plus l'event loop
* - Vrai parallélisme pour fanout/broadcast (Promise.all)
* - Support dual: POST /rpc (JSON-RPC) + POST /send (legacy)
* - Port scan: 3101-3120 (NEXUS) + 3001-3020 (legacy)
* - Profils Hermes: ~/.overmind/hermes/ + ~/AppData/Local/hermes/
* - 127.0.0.1 au lieu de localhost (IPv4/IPv6 fix)
*/
import { z } from 'zod';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { randomUUID } from 'crypto';
// ─── HTTP Helpers (async fetch — non-blocking) ─────────────────────────────
async function httpGet(url, timeoutMs = 5000) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(url, { method: 'GET', signal: controller.signal });
clearTimeout(timer);
if (!response.ok)
return null;
const text = await response.text();
return JSON.parse(text);
}
catch {
return null;
}
}
async function httpPost(url, body, timeoutMs = 300000) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: controller.signal,
});
clearTimeout(timer);
if (!response.ok)
return null;
const text = await response.text();
return JSON.parse(text);
}
catch {
return null;
}
}
// ─── Bridge Discovery ──────────────────────────────────────────────────────
/** Get candidate Hermes profile directories (Windows + Linux) */
function getProfileDirs() {
const dirs = [];
const home = os.homedir();
// Linux/Ubuntu: ~/.overmind/hermes/profiles/
const overmindHermes = path.join(home, '.overmind', 'hermes', 'profiles');
if (fs.existsSync(overmindHermes))
dirs.push(overmindHermes);
// Windows: ~/AppData/Local/hermes/profiles/
const winHermes = path.join(home, 'AppData', 'Local', 'hermes', 'profiles');
if (fs.existsSync(winHermes))
dirs.push(winHermes);
// Linux native: ~/.hermes/profiles/
const nativeHermes = path.join(home, '.hermes', 'profiles');
if (fs.existsSync(nativeHermes))
dirs.push(nativeHermes);
return dirs;
}
/** Get known bridge ports from pid files */
function getKnownPorts() {
const known = [];
const home = os.homedir();
// 1. NEXUS pid file: Nexus/tmp/bridge_pids.json
const nexusPidPath = path.join(home, 'Nexus', 'tmp', 'bridge_pids.json');
if (fs.existsSync(nexusPidPath)) {
try {
const data = JSON.parse(fs.readFileSync(nexusPidPath, 'utf8'));
if (Array.isArray(data)) {
for (const entry of data) {
if (entry.port && entry.name) {
known.push({ port: entry.port, agentName: entry.name });
}
}
}
else if (data.workers && Array.isArray(data.workers)) {
for (const w of data.workers) {
if (w.port)
known.push({ port: w.port, agentName: w.agentName || '' });
}
}
}
catch { /* ignore */ }
}
// 2. Legacy workers.json: ~/.overmind/bridge/workers.json
const workersJsonPath = path.join(home, '.overmind', 'bridge', 'workers.json');
if (fs.existsSync(workersJsonPath)) {
try {
const data = JSON.parse(fs.readFileSync(workersJsonPath, 'utf8'));
if (Array.isArray(data.workers)) {
for (const w of data.workers) {
if (w.port)
known.push({ port: w.port, agentName: w.agentName || '' });
}
}
}
catch { /* ignore */ }
}
return known;
}
/**
* Probe a single port for bridge health.
*
* Fix #5: Tries IPv4 (127.0.0.1) first, then IPv6 ([::1]) — because some
* NEXUS bridges bind on [::1] only (FastMCP default on Windows).
*/
async function probeBridge(port, agentName) {
// Try IPv4 first, then IPv6 loopback
const urls = [`http://127.0.0.1:${port}`, `http://[::1]:${port}`];
for (const url of urls) {
const health = await httpGet(`${url}/health`, 3000);
if (health) {
const detectedAgent = agentName ||
health.agent ||
health.agentName ||
health.service ||
'unknown';
// NEXUS bridges expose rpcMethods[] in health — they support POST /rpc
const hasRpc = Array.isArray(health.rpcMethods) || 'jsonrpc' in health;
return { port, url, online: true, agentName: detectedAgent, health, hasRpc };
}
}
return { port, url: `http://127.0.0.1:${port}`, online: false, agentName: agentName || '', hasRpc: false };
}
/** Discover all bridges (scan ports + pid files) */
async function discoverBridges() {
const knownPorts = getKnownPorts();
// Build port list: known ports + scan ranges
const portSet = new Set();
const portAgentMap = new Map();
// Add known ports from pid files
for (const { port, agentName } of knownPorts) {
portSet.add(port);
if (agentName)
portAgentMap.set(port, agentName);
}
// NEXUS range: 3101-3120
for (let p = 3101; p <= 3120; p++)
portSet.add(p);
// Legacy range: 3001-3020
for (let p = 3001; p <= 3020; p++)
portSet.add(p);
// Probe all ports in parallel (true parallel — async fetch)
const ports = Array.from(portSet).sort((a, b) => a - b);
const results = await Promise.all(ports.map((port) => probeBridge(port, portAgentMap.get(port) || '')));
return results;
}
// ─── Agent Discovery ───────────────────────────────────────────────────────
async function discoverAgents() {
const selfAgent = process.env.OVERMIND_AGENT_NAME || null;
const agents = [];
const bridges = await discoverBridges();
// Build bridge lookup: agentName → bridge
const bridgeByAgent = new Map();
for (const b of bridges) {
if (b.online && b.agentName && b.agentName !== 'unknown') {
bridgeByAgent.set(b.agentName, b);
}
}
// Scan all profile directories
for (const profilesDir of getProfileDirs()) {
let entries;
try {
entries = fs.readdirSync(profilesDir, { withFileTypes: true });
}
catch {
continue;
}
for (const entry of entries) {
if (!entry.isDirectory())
continue;
const profilePath = path.join(profilesDir, entry.name);
// Avoid duplicates (same profile name in different dirs)
if (agents.some((a) => a.name === entry.name))
continue;
// Read config.yaml
const configPath = path.join(profilePath, 'config.yaml');
let model = 'unknown';
let provider = 'unknown';
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, 'utf8');
const modelMatch = configContent.match(/^\s*model:\s*(?!provider)(.+)/m);
const providerMatch = configContent.match(/^\s*provider:\s*(.+)/m);
if (modelMatch)
model = modelMatch[1].trim().replace(/['"]/g, '');
if (providerMatch)
provider = providerMatch[1].trim().replace(/['"]/g, '');
}
// Read profile.yaml for description
const profileYamlPath = path.join(profilePath, 'profile.yaml');
let description = '';
if (fs.existsSync(profileYamlPath)) {
const py = fs.readFileSync(profileYamlPath, 'utf8');
const descMatch = py.match(/description:\s*"?([^"\n]+)"?/);
if (descMatch)
description = descMatch[1].trim();
}
// Count skills
let skillsCount = 0;
const skillsDir = path.join(profilePath, 'skills');
if (fs.existsSync(skillsDir)) {
try {
const walk = (dir) => {
let count = 0;
for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
if (f.isDirectory())
count += walk(path.join(dir, f.name));
else if (f.name === 'SKILL.md')
count++;
}
return count;
};
skillsCount = walk(skillsDir);
}
catch { /* ignore */ }
}
// Check memory (state.db or MEMORY.md)
const hasMemory = fs.existsSync(path.join(profilePath, 'memories', 'state.db')) ||
fs.existsSync(path.join(profilePath, 'memories', 'MEMORY.md'));
// Last activity
let lastActivity = null;
const stateDb = path.join(profilePath, 'memories', 'state.db');
if (fs.existsSync(stateDb)) {
try {
lastActivity = fs.statSync(stateDb).mtime.toISOString();
}
catch { /* ignore */ }
}
// Cross-reference with bridges
const bridge = bridgeByAgent.get(entry.name);
agents.push({
name: entry.name,
model,
provider,
status: bridge ? 'online' : 'offline',
bridgePort: bridge?.port ?? null,
bridgeUrl: bridge?.url ?? null,
description,
skillsCount,
hasMemory,
lastActivity,
});
}
}
return {
totalAgents: agents.length,
onlineBridges: bridges.filter((b) => b.online).length,
agents,
bridges,
selfAgent,
};
}
// ─── Send message to a bridge (supports /rpc + /send) ──────────────────────
async function sendToBridge(bridge, message, opts = {}) {
const selfAgent = process.env.OVERMIND_AGENT_NAME || 'A2A Hub';
const enrichedMessage = `[A2A — Message from ${selfAgent}]\n${message}`;
const timeout = opts.timeoutMs ?? 300000;
// Strategy 1: NEXUS bridge with POST /rpc (JSON-RPC 2.0)
if (bridge.hasRpc) {
const rpcBody = {
jsonrpc: '2.0',
id: randomUUID(),
method: 'agent.run',
params: {
agentName: bridge.agentName,
runner: 'hermes',
prompt: enrichedMessage,
...(opts.model ? { model: opts.model } : {}),
},
};
const response = await httpPost(`${bridge.url}/rpc`, rpcBody, timeout);
if (response === null) {
return { success: false, text: `Bridge :${bridge.port} injoignable`, raw: null };
}
if (response.error) {
return { success: false, text: `Erreur :${bridge.port}: ${response.error.message || JSON.stringify(response.error)}`, raw: response };
}
// Extract text from JSON-RPC result
const result = response.result;
if (result) {
const content = result.content;
const text = content
? content.map((c) => c.text || '').join('\n')
: result.output || result.text || JSON.stringify(result);
return { success: true, text, raw: response };
}
return { success: true, text: JSON.stringify(response), raw: response };
}
// Strategy 2: Legacy bridge with POST /send
const sendBody = {
message: enrichedMessage,
userId: `a2a_${selfAgent}`,
username: selfAgent,
channelId: 'a2a',
};
if (opts.model)
sendBody.model = opts.model;
const response = await httpPost(`${bridge.url}/send`, sendBody, timeout);
if (response === null) {
return { success: false, text: `Bridge :${bridge.port} injoignable`, raw: null };
}
if (response.error) {
return { success: false, text: `Erreur :${bridge.port}: ${response.error}`, raw: response };
}
const text = response.result ||
response.response ||
response.output ||
(Array.isArray(response.content)
? response.content.map((c) => c.text || '').join('\n')
: '') ||
JSON.stringify(response);
return { success: true, text, raw: response };
}
// ─── Schema ────────────────────────────────────────────────────────────────
export const a2aHubSchema = z.object({
action: z
.enum(['discover', 'status', 'send', 'delegate', 'pipeline', 'fanout', 'query', 'broadcast'])
.describe("Action: discover=liste tous les agents+bridges, status=état d'un bridge, send=message synchrone, delegate=async, pipeline=chaîne A→B→C, fanout=1→N+merge, query=multi-agents, broadcast=global"),
target: z
.string()
.optional()
.describe("Nom de l'agent cible (ex: 'nexus_master') OU port du bridge (ex: '3101')"),
message: z.string().optional().describe('Le message/prompt à envoyer aux agents'),
targets: z
.array(z.string())
.optional()
.describe('Liste des agents cibles (noms ou ports) pour fanout/query/broadcast'),
steps: z
.array(z.object({
agentName: z
.string()
.describe("Nom de l'agent ou port (ex: 'nexus_master' ou '3101')"),
promptPrefix: z.string().optional(),
}))
.optional()
.describe('Étapes de la pipeline (pour action=pipeline)'),
mergeStrategy: z
.enum(['concat', 'best', 'vote', 'first_success'])
.optional()
.default('concat')
.describe('Stratégie de merge pour fanout (default: concat)'),
race: z
.boolean()
.optional()
.default(false)
.describe('Pour broadcast: si true, premier qui répond gagne'),
async: z
.boolean()
.optional()
.default(true)
.describe('Pour delegate: si true (default), retourne immédiatement'),
accumulateContext: z
.boolean()
.optional()
.default(false)
.describe('Pour pipeline: chaque step reçoit tous les outputs précédents'),
timeoutMs: z
.number()
.int()
.min(5000)
.max(600000)
.optional()
.describe('Timeout par agent en ms (default: 300000 = 5min)'),
model: z.string().optional().describe('Modèle LLM override'),
});
// ─── Resolve agent name to bridge ──────────────────────────────────────────
function resolveTarget(target, discovery) {
// If target is a port number
const portMatch = target.match(/^(\d{4,5})$/);
if (portMatch) {
const port = parseInt(portMatch[1], 10);
const bridge = discovery.bridges.find((b) => b.port === port);
return { bridge: bridge ?? null, agentName: bridge?.agentName || target };
}
// If target is an agent name, find its bridge
const agent = discovery.agents.find((a) => a.name === target);
if (agent?.bridgePort) {
const bridge = discovery.bridges.find((b) => b.port === agent.bridgePort);
if (bridge)
return { bridge, agentName: target };
}
// Try online bridges by agentName
const bridge = discovery.bridges.find((b) => b.online && b.agentName === target);
if (bridge)
return { bridge, agentName: target };
return { bridge: null, agentName: target };
}
// ─── Execute ───────────────────────────────────────────────────────────────
export async function a2aHub(args) {
const selfAgent = process.env.OVERMIND_AGENT_NAME || 'unknown';
const timeout = args.timeoutMs ?? 300000;
try {
switch (args.action) {
// ═══════════════════════════════════════════════════════════════════════
// DISCOVER
// ═══════════════════════════════════════════════════════════════════════
case 'discover': {
const d = await discoverAgents();
const lines = [
`🌐 **A2A Hub — Découverte du système multi-agents**`,
``,
`**Self:** ${d.selfAgent || '(inconnu)'}`,
`**Bridges online:** ${d.onlineBridges}`,
`**Total agents:** ${d.totalAgents}`,
``,
`### Bridges HTTP`,
``,
];
const onlineBridges = d.bridges.filter((b) => b.online);
if (onlineBridges.length === 0) {
lines.push('Aucun bridge HTTP en ligne.');
}
else {
lines.push('| Port | Agent | Type | URL |');
lines.push('|------|-------|------|-----|');
for (const b of onlineBridges) {
const type = b.hasRpc ? 'RPC' : 'Legacy';
lines.push(`| :${b.port} | ${b.agentName} | ${type} | ${b.url} |`);
}
}
lines.push('');
lines.push('### Agents');
lines.push('');
if (d.agents.length === 0) {
lines.push('Aucun agent trouvé.');
}
else {
lines.push('| Agent | Status | Bridge | Model | Provider | Skills | Description |');
lines.push('|-------|--------|--------|-------|----------|--------|-------------|');
for (const a of d.agents) {
const statusIcon = a.status === 'online' ? '🟢' : '🔴';
const bridgeInfo = a.bridgePort ? `:${a.bridgePort}` : '—';
const nameDisplay = a.name === selfAgent ? `**${a.name} (self)**` : a.name;
lines.push(`| ${nameDisplay} | ${statusIcon} ${a.status} | ${bridgeInfo} | ${a.model} | ${a.provider} | ${a.skillsCount} | ${a.description.slice(0, 40)} |`);
}
}
lines.push('');
lines.push('**Actions disponibles:** send, delegate, pipeline, fanout, query, broadcast');
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
// ═══════════════════════════════════════════════════════════════════════
// STATUS
// ═══════════════════════════════════════════════════════════════════════
case 'status': {
if (!args.target) {
return {
content: [{ type: 'text', text: '❌ `target` requis (nom ou port)' }],
isError: true,
};
}
const d = await discoverAgents();
const { bridge, agentName } = resolveTarget(args.target, d);
if (!bridge || !bridge.online) {
return {
content: [{ type: 'text', text: `❌ Bridge pour "${args.target}" introuvable` }],
isError: true,
};
}
const lines = [`📊 **Status: ${agentName} (:${bridge.port})**`, ``];
for (const [key, value] of Object.entries(bridge.health ?? {})) {
lines.push(`**${key}:** ${typeof value === 'object' ? JSON.stringify(value).slice(0, 100) : value}`);
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
// ═══════════════════════════════════════════════════════════════════════
// SEND
// ═══════════════════════════════════════════════════════════════════════
case 'send': {
if (!args.target || !args.message) {
return {
content: [{ type: 'text', text: '❌ `target` et `message` requis' }],
isError: true,
};
}
const d = await discoverAgents();
const { bridge, agentName } = resolveTarget(args.target, d);
if (!bridge || !bridge.online) {
return {
content: [{ type: 'text', text: `❌ Bridge pour "${args.target}" introuvable. Utilisez action=discover.` }],
isError: true,
};
}
const result = await sendToBridge(bridge, args.message, {
model: args.model,
timeoutMs: timeout,
});
if (!result.success) {
return {
content: [{ type: 'text', text: `❌ ${result.text}` }],
isError: true,
};
}
return {
content: [{ type: 'text', text: `📤 **Message envoyé à ${agentName} (:${bridge.port})**\n\n${result.text}` }],
};
}
// ═══════════════════════════════════════════════════════════════════════
// DELEGATE (async — fire and forget)
// ═══════════════════════════════════════════════════════════════════════
case 'delegate': {
if (!args.target || !args.message) {
return {
content: [{ type: 'text', text: '❌ `target` et `message` requis' }],
isError: true,
};
}
const d = await discoverAgents();
const { bridge, agentName } = resolveTarget(args.target, d);
if (!bridge || !bridge.online) {
return {
content: [{ type: 'text', text: `❌ Bridge pour "${args.target}" introuvable` }],
isError: true,
};
}
// Fire and forget — don't await
void sendToBridge(bridge, args.message, { model: args.model, timeoutMs: timeout })
.catch(() => { });
return {
content: [{
type: 'text',
text: `🤝 **Tâche déléguée à ${agentName} (:${bridge.port})**\n\nLe bridge traite la requête en arrière-plan.`,
}],
};
}
// ═══════════════════════════════════════════════════════════════════════
// PIPELINE
// ═══════════════════════════════════════════════════════════════════════
case 'pipeline': {
if (!args.message || !args.steps || args.steps.length === 0) {
return {
content: [{ type: 'text', text: '❌ `message` (prompt initial) et `steps` requis' }],
isError: true,
};
}
const d = await discoverAgents();
const outputs = [];
let currentPrompt = args.message;
for (let i = 0; i < args.steps.length; i++) {
const step = args.steps[i];
const { bridge, agentName } = resolveTarget(step.agentName, d);
if (!bridge || !bridge.online) {
outputs.push({ agent: step.agentName, output: 'Bridge introuvable', success: false });
break;
}
const stepPrompt = (step.promptPrefix ? step.promptPrefix + '\n\n' : '') +
`[Pipeline Step ${i + 1}/${args.steps.length}]\n${currentPrompt}`;
const result = await sendToBridge(bridge, stepPrompt, { timeoutMs: timeout });
outputs.push({
agent: `${agentName} (:${bridge.port})`,
output: result.text,
success: result.success,
});
if (!result.success)
break;
currentPrompt = args.accumulateContext
? outputs.map((o) => `[${o.agent}]: ${o.output}`).join('\n\n---\n\n')
: result.text;
}
const lines = [`🔗 **Pipeline** (${outputs.length}/${args.steps.length} steps)`, ``];
for (const o of outputs) {
lines.push(`**${o.success ? '✅' : '❌'} ${o.agent}:**`);
lines.push(o.output.slice(0, 800) + (o.output.length > 800 ? '...' : ''));
lines.push('');
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
// ═══════════════════════════════════════════════════════════════════════
// FANOUT (true parallel — Promise.all)
// ═══════════════════════════════════════════════════════════════════════
case 'fanout': {
if (!args.message || !args.targets || args.targets.length === 0) {
return {
content: [{ type: 'text', text: '❌ `message` et `targets` requis' }],
isError: true,
};
}
const d = await discoverAgents();
// Run ALL targets in parallel (true parallel — async fetch)
const results = await Promise.all(args.targets.map(async (target) => {
const { bridge, agentName } = resolveTarget(target, d);
if (!bridge || !bridge.online) {
return { agent: target, success: false, text: 'Bridge introuvable' };
}
const r = await sendToBridge(bridge, args.message, {
model: args.model,
timeoutMs: timeout,
});
return { agent: `${agentName} (:${bridge.port})`, success: r.success, text: r.text };
}));
// Merge
let merged;
let winner;
switch (args.mergeStrategy) {
case 'first_success': {
const first = results.find((r) => r.success);
merged = first ? first.text : 'Tous ont échoué';
winner = first?.agent;
break;
}
case 'best': {
const sorted = results
.filter((r) => r.success)
.sort((a, b) => b.text.length - a.text.length);
merged = sorted.length > 0 ? sorted[0].text : 'Tous ont échoué';
winner = sorted[0]?.agent;
break;
}
case 'concat':
default: {
merged = results
.map((r) => `### ${r.agent}${r.success ? '' : ' (ÉCHEC)'}\n${r.text}`)
.join('\n\n---\n\n');
break;
}
}
const successCount = results.filter((r) => r.success).length;
const lines = [
`🌐 **Fanout** (${successCount}/${results.length} succès, merge=${args.mergeStrategy})`,
...(winner ? [`**Gagnant:** ${winner}`] : []),
``,
merged.slice(0, 3000),
];
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
// ═══════════════════════════════════════════════════════════════════════
// QUERY (parallel, short timeout)
// ═══════════════════════════════════════════════════════════════════════
case 'query': {
if (!args.message || !args.targets || args.targets.length === 0) {
return {
content: [{ type: 'text', text: '❌ `message` et `targets` requis' }],
isError: true,
};
}
const d = await discoverAgents();
const queryTimeout = Math.min(timeout, 60000);
const results = await Promise.all(args.targets.map(async (target) => {
const { bridge, agentName } = resolveTarget(target, d);
if (!bridge || !bridge.online) {
return { agent: target, success: false, text: 'Bridge introuvable' };
}
const r = await sendToBridge(bridge, args.message, {
model: args.model,
timeoutMs: queryTimeout,
});
return { agent: `${agentName} (:${bridge.port})`, success: r.success, text: r.text };
}));
const lines = [
`❓ **Query** (${results.filter((r) => r.success).length}/${results.length} réponses)`,
``,
];
for (const r of results) {
lines.push(`**${r.success ? '✅' : '❌'} ${r.agent}:**`);
lines.push(r.text.slice(0, 500) + (r.text.length > 500 ? '...' : ''));
lines.push('');
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
// ═══════════════════════════════════════════════════════════════════════
// BROADCAST
// ═══════════════════════════════════════════════════════════════════════
case 'broadcast': {
if (!args.message) {
return {
content: [{ type: 'text', text: '❌ `message` requis' }],
isError: true,
};
}
const d = await discoverAgents();
let targetBridges;
if (args.targets && args.targets.length > 0) {
targetBridges = args.targets
.map((t) => resolveTarget(t, d).bridge)
.filter((b) => b !== null && b.online);
}
else {
targetBridges = d.bridges.filter((b) => b.online);
}
if (targetBridges.length === 0) {
return {
content: [{ type: 'text', text: '❌ Aucun bridge online pour broadcast' }],
isError: true,
};
}
if (args.race) {
// First to respond wins — use Promise.any
try {
const firstResult = await Promise.any(targetBridges.map(async (b) => {
const r = await sendToBridge(b, args.message, { model: args.model, timeoutMs: timeout });
if (!r.success)
throw new Error(r.text);
return { bridge: b, text: r.text };
}));
return {
content: [{
type: 'text',
text: `📡 **Broadcast race — ${firstResult.bridge.agentName} (:${firstResult.bridge.port}) a gagné!**\n\n${firstResult.text}`,
}],
};
}
catch {
return {
content: [{ type: 'text', text: '❌ Tous les bridges ont échoué' }],
isError: true,
};
}
}
// Send to all in parallel
const results = await Promise.all(targetBridges.map(async (b) => {
const r = await sendToBridge(b, args.message, { model: args.model, timeoutMs: timeout });
return { agent: `${b.agentName} (:${b.port})`, success: r.success, text: r.text };
}));
const successCount = results.filter((r) => r.success).length;
const lines = [`📡 **Broadcast** — ${successCount}/${results.length} succès`, ``];
for (const r of results) {
lines.push(`**${r.success ? '✅' : '❌'} ${r.agent}:**`);
lines.push(r.text.slice(0, 300) + (r.text.length > 300 ? '...' : ''));
lines.push('');
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
default:
return {
content: [{ type: 'text', text: `❌ Action inconnue: ${args.action}` }],
isError: true,
};
}
}
catch (error) {
return {
content: [{
type: 'text',
text: `❌ Erreur A2A Hub: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
//# sourceMappingURL=a2a_hub.js.map