overmind-mcp 3.4.1 → 3.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install-overmind-unix.sh +2 -2
- package/bin/install-overmind-windows.bat +2 -2
- package/dist/bin/overmind-bridge.js +2 -1
- package/dist/bin/overmind-bridge.js.map +1 -1
- package/dist/bridge/MessageLog.js +1 -1
- package/dist/bridge/MessageLog.js.map +1 -1
- package/dist/tools/a2a_hub.d.ts +14 -22
- package/dist/tools/a2a_hub.d.ts.map +1 -1
- package/dist/tools/a2a_hub.js +440 -363
- package/dist/tools/a2a_hub.js.map +1 -1
- package/package.json +1 -1
- package/scripts/auto-install.mjs +4 -2
- package/scripts/install-dependencies.mjs +5 -3
- package/scripts/postinstall.mjs +118 -2
- package/scripts/setup-windows.js +1 -1
- package/scripts/setup.mjs +5 -2
- package/scripts/verify-install.mjs +27 -2
- package/scripts/migrate-hermes-home.mjs +0 -133
- package/scripts/migrate-to-profiles.mjs +0 -205
package/dist/tools/a2a_hub.js
CHANGED
|
@@ -1,44 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
-
* A2A Hub — Outil MCP
|
|
3
|
+
* A2A Hub — Outil MCP pour la communication Agent-to-Agent
|
|
4
4
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Architecture distribuee — chaque worker est un serveur HTTP independant:
|
|
7
7
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* - DELEGUER une tâche à un autre agent (async avec callback)
|
|
11
|
-
* - PIPELINE chainé (A→B→C avec passing de contexte)
|
|
12
|
-
* - FANOUT parallèle (1→N + merge des résultats)
|
|
13
|
-
* - QUERY multi-agents (poser une question à plusieurs agents simultanément)
|
|
14
|
-
* - BROADCAST (message global à tous les agents online)
|
|
8
|
+
* discord-master (:discord) → routes !trade → TV Analyst :3002
|
|
9
|
+
* → routes !sniper → Sniperbot :3001
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - Leur runner (hermes, claude, kilo, etc.)
|
|
20
|
-
* - Leur modèle LLM
|
|
21
|
-
* - Leur dernière activité
|
|
22
|
-
* - Le compteur A2A (combien de messages envoyés/reçus)
|
|
11
|
+
* a2a_hub parle DIRECTEMENT aux workers HTTP:
|
|
12
|
+
* POST :300X/send → message synchrone
|
|
13
|
+
* GET :300X/health → status live
|
|
23
14
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
15
|
+
* Decouverte automatique:
|
|
16
|
+
* 1. Scan ports 3001-3020 (workers connus)
|
|
17
|
+
* 2. Scan ~/.overmind/hermes/profiles/ (agents Hermes)
|
|
18
|
+
* 3. Cross-reference: profile ↔ worker port
|
|
19
|
+
* 4. Lit ~/.overmind/bridge/workers.json si present
|
|
26
20
|
*/
|
|
27
21
|
import { z } from 'zod';
|
|
28
22
|
import { execSync } from 'child_process';
|
|
29
23
|
import fs from 'fs';
|
|
30
24
|
import path from 'path';
|
|
31
25
|
import os from 'os';
|
|
32
|
-
// ───
|
|
26
|
+
// ─── HTTP Helper (curl synchrone) ──────────────────────────────────────────
|
|
27
|
+
function httpGet(url, timeoutMs = 5000) {
|
|
28
|
+
try {
|
|
29
|
+
const result = execSync(`curl -s -m ${Math.floor(timeoutMs / 1000)} "${url}" 2>/dev/null`, {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
timeout: timeoutMs + 1000,
|
|
32
|
+
});
|
|
33
|
+
return JSON.parse(result);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function httpPost(url, body, timeoutMs = 300000) {
|
|
40
|
+
const payload = JSON.stringify(body);
|
|
41
|
+
try {
|
|
42
|
+
const escapedPayload = payload.replace(/'/g, "'\\''");
|
|
43
|
+
const result = execSync(`curl -s -m ${Math.floor(timeoutMs / 1000)} -X POST "${url}" -H "Content-Type: application/json" -d '${escapedPayload}' 2>/dev/null`, { encoding: 'utf8', timeout: timeoutMs + 1000, maxBuffer: 50 * 1024 * 1024 });
|
|
44
|
+
return JSON.parse(result);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ─── Worker Discovery ──────────────────────────────────────────────────────
|
|
51
|
+
function discoverWorkers() {
|
|
52
|
+
const workers = [];
|
|
53
|
+
const workersJsonPath = path.join(os.homedir(), '.overmind', 'bridge', 'workers.json');
|
|
54
|
+
// 1. Try reading workers registry
|
|
55
|
+
let knownPorts = [];
|
|
56
|
+
if (fs.existsSync(workersJsonPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(fs.readFileSync(workersJsonPath, 'utf8'));
|
|
59
|
+
if (Array.isArray(data.workers)) {
|
|
60
|
+
knownPorts = data.workers.map((w) => ({
|
|
61
|
+
port: w.port,
|
|
62
|
+
agentName: w.agentName || 'unknown',
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 2. Fallback: scan ports 3001-3020
|
|
71
|
+
if (knownPorts.length === 0) {
|
|
72
|
+
knownPorts = [];
|
|
73
|
+
for (let port = 3001; port <= 3020; port++) {
|
|
74
|
+
knownPorts.push({ port, agentName: '' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// 3. Probe each port
|
|
78
|
+
for (const { port, agentName } of knownPorts) {
|
|
79
|
+
const url = `http://localhost:${port}`;
|
|
80
|
+
const health = httpGet(`${url}/health`, 3000);
|
|
81
|
+
if (health) {
|
|
82
|
+
// Worker is online — extract agent name from health
|
|
83
|
+
const detectedAgent = agentName ||
|
|
84
|
+
health.agent ||
|
|
85
|
+
health.agentName ||
|
|
86
|
+
health.service ||
|
|
87
|
+
'unknown';
|
|
88
|
+
workers.push({
|
|
89
|
+
port,
|
|
90
|
+
url,
|
|
91
|
+
online: true,
|
|
92
|
+
agentName: detectedAgent,
|
|
93
|
+
health,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Check if port is listening at all
|
|
98
|
+
workers.push({
|
|
99
|
+
port,
|
|
100
|
+
url,
|
|
101
|
+
online: false,
|
|
102
|
+
agentName: agentName || '',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return workers;
|
|
107
|
+
}
|
|
108
|
+
// ─── Agent Discovery ───────────────────────────────────────────────────────
|
|
33
109
|
function discoverAgents() {
|
|
34
110
|
const home = os.homedir();
|
|
35
|
-
const profilesDir =
|
|
36
|
-
? path.join(home, '.overmind', 'hermes', 'profiles')
|
|
37
|
-
: path.join(home, '.overmind', 'hermes', 'profiles');
|
|
38
|
-
const bridgeUrl = process.env.OVERMIND_BRIDGE_URL || 'http://localhost:3100/rpc';
|
|
111
|
+
const profilesDir = path.join(home, '.overmind', 'hermes', 'profiles');
|
|
39
112
|
const selfAgent = process.env.OVERMIND_AGENT_NAME || null;
|
|
40
113
|
const agents = [];
|
|
41
|
-
|
|
114
|
+
const workers = discoverWorkers();
|
|
115
|
+
// Build worker lookup: agentName → worker
|
|
116
|
+
const workerByAgent = new Map();
|
|
117
|
+
for (const w of workers) {
|
|
118
|
+
if (w.online && w.agentName && w.agentName !== 'unknown') {
|
|
119
|
+
workerByAgent.set(w.agentName, w);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Scan Hermes profiles
|
|
42
123
|
if (fs.existsSync(profilesDir)) {
|
|
43
124
|
const entries = fs.readdirSync(profilesDir, { withFileTypes: true });
|
|
44
125
|
for (const entry of entries) {
|
|
@@ -51,12 +132,12 @@ function discoverAgents() {
|
|
|
51
132
|
let provider = 'unknown';
|
|
52
133
|
if (fs.existsSync(configPath)) {
|
|
53
134
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
54
|
-
const modelMatch = config.match(
|
|
55
|
-
const providerMatch = config.match(
|
|
135
|
+
const modelMatch = config.match(/^\s*model:\s*(?!provider)(.+)/m);
|
|
136
|
+
const providerMatch = config.match(/^\s*provider:\s*(.+)/m);
|
|
56
137
|
if (modelMatch)
|
|
57
|
-
model = modelMatch[1].trim();
|
|
138
|
+
model = modelMatch[1].trim().replace(/['"]/g, '');
|
|
58
139
|
if (providerMatch)
|
|
59
|
-
provider = providerMatch[1].trim();
|
|
140
|
+
provider = providerMatch[1].trim().replace(/['"]/g, '');
|
|
60
141
|
}
|
|
61
142
|
// Read profile.yaml for description
|
|
62
143
|
const profileYamlPath = path.join(profilePath, 'profile.yaml');
|
|
@@ -92,119 +173,96 @@ function discoverAgents() {
|
|
|
92
173
|
}
|
|
93
174
|
// Check memory
|
|
94
175
|
const hasMemory = fs.existsSync(path.join(profilePath, 'memories', 'MEMORY.md'));
|
|
95
|
-
// Last activity
|
|
176
|
+
// Last activity
|
|
96
177
|
let lastActivity = null;
|
|
97
178
|
const stateDb = path.join(profilePath, 'state.db');
|
|
98
179
|
if (fs.existsSync(stateDb)) {
|
|
99
180
|
try {
|
|
100
|
-
|
|
101
|
-
lastActivity = stat.mtime.toISOString();
|
|
181
|
+
lastActivity = fs.statSync(stateDb).mtime.toISOString();
|
|
102
182
|
}
|
|
103
183
|
catch {
|
|
104
184
|
// ignore
|
|
105
185
|
}
|
|
106
186
|
}
|
|
187
|
+
// Cross-reference with workers
|
|
188
|
+
const worker = workerByAgent.get(entry.name);
|
|
107
189
|
agents.push({
|
|
108
190
|
name: entry.name,
|
|
109
|
-
runner: 'hermes',
|
|
110
191
|
model,
|
|
111
|
-
status: 'unknown', // Will be enriched from bridge if available
|
|
112
192
|
provider,
|
|
113
|
-
|
|
193
|
+
status: worker ? 'online' : 'offline',
|
|
194
|
+
workerPort: worker?.port ?? null,
|
|
195
|
+
workerUrl: worker?.url ?? null,
|
|
114
196
|
description,
|
|
115
197
|
skillsCount,
|
|
116
198
|
hasMemory,
|
|
117
|
-
|
|
199
|
+
lastActivity,
|
|
118
200
|
});
|
|
119
201
|
}
|
|
120
202
|
}
|
|
121
|
-
// 2. Try to get live status from bridge
|
|
122
|
-
let bridgeOnline = false;
|
|
123
|
-
try {
|
|
124
|
-
const healthUrl = bridgeUrl.replace('/rpc', '/health');
|
|
125
|
-
const result = execSync(`curl -s -m 3 "${healthUrl}" 2>/dev/null || echo '{}'`, {
|
|
126
|
-
encoding: 'utf8',
|
|
127
|
-
timeout: 5000,
|
|
128
|
-
});
|
|
129
|
-
const health = JSON.parse(result);
|
|
130
|
-
bridgeOnline = health.status === 'online' || health.status === 'degraded';
|
|
131
|
-
// Enrich agents with live status from registry
|
|
132
|
-
if (health.agents && Array.isArray(health.agents)) {
|
|
133
|
-
for (const liveAgent of health.agents) {
|
|
134
|
-
const found = agents.find((a) => a.name === liveAgent.name);
|
|
135
|
-
if (found) {
|
|
136
|
-
found.status = liveAgent.status;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// Bridge not reachable — agents keep 'unknown' status
|
|
143
|
-
}
|
|
144
|
-
// Default 'offline' for agents we couldn't reach
|
|
145
|
-
for (const a of agents) {
|
|
146
|
-
if (a.status === 'unknown')
|
|
147
|
-
a.status = 'offline';
|
|
148
|
-
}
|
|
149
203
|
return {
|
|
150
204
|
totalAgents: agents.length,
|
|
151
|
-
|
|
152
|
-
busyAgents: agents.filter((a) => a.status === 'busy').length,
|
|
205
|
+
onlineWorkers: workers.filter((w) => w.online).length,
|
|
153
206
|
agents,
|
|
207
|
+
workers,
|
|
154
208
|
selfAgent,
|
|
155
|
-
bridgeUrl,
|
|
156
|
-
bridgeOnline,
|
|
157
209
|
};
|
|
158
210
|
}
|
|
159
|
-
// ───
|
|
160
|
-
function
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
error: response.error.message,
|
|
174
|
-
code: response.error.code,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
return response.result;
|
|
211
|
+
// ─── Send message to a worker ──────────────────────────────────────────────
|
|
212
|
+
function sendToWorker(port, message, opts = {}) {
|
|
213
|
+
const url = `http://localhost:${port}/send`;
|
|
214
|
+
const body = {
|
|
215
|
+
message,
|
|
216
|
+
userId: `a2a_${process.env.OVERMIND_AGENT_NAME || 'system'}`,
|
|
217
|
+
username: process.env.OVERMIND_AGENT_NAME || 'A2A Hub',
|
|
218
|
+
channelId: 'a2a',
|
|
219
|
+
};
|
|
220
|
+
if (opts.model)
|
|
221
|
+
body.model = opts.model;
|
|
222
|
+
const response = httpPost(url, body, opts.timeoutMs ?? 300000);
|
|
223
|
+
if (response === null) {
|
|
224
|
+
return { success: false, text: `Worker :${port} injoignable`, raw: null };
|
|
178
225
|
}
|
|
179
|
-
|
|
180
|
-
return {
|
|
226
|
+
if (response.error) {
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
text: `Erreur :${port}: ${response.error}`,
|
|
230
|
+
raw: response,
|
|
231
|
+
};
|
|
181
232
|
}
|
|
233
|
+
// Workers may return { result, content, response, output }
|
|
234
|
+
const text = response.result ||
|
|
235
|
+
response.response ||
|
|
236
|
+
response.output ||
|
|
237
|
+
(Array.isArray(response.content)
|
|
238
|
+
? response.content.map((c) => c.text || '').join('\n')
|
|
239
|
+
: '') ||
|
|
240
|
+
JSON.stringify(response);
|
|
241
|
+
return { success: true, text, raw: response };
|
|
182
242
|
}
|
|
183
243
|
// ─── Schema ────────────────────────────────────────────────────────────────
|
|
184
244
|
export const a2aHubSchema = z.object({
|
|
185
245
|
action: z
|
|
186
246
|
.enum(['discover', 'status', 'send', 'delegate', 'pipeline', 'fanout', 'query', 'broadcast'])
|
|
187
|
-
.describe("Action
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
247
|
+
.describe("Action: discover=liste tous les agents+workers, status=état d'un worker, send=message synchrone, delegate=async, pipeline=chaîne A→B→C, fanout=1→N+merge, query=multi-agents, broadcast=global"),
|
|
248
|
+
target: z
|
|
249
|
+
.string()
|
|
250
|
+
.optional()
|
|
251
|
+
.describe("Nom de l'agent cible (ex: 'sniperbot_analyst') OU port du worker (ex: '3001')"),
|
|
191
252
|
message: z.string().optional().describe('Le message/prompt à envoyer aux agents'),
|
|
192
|
-
// ─── Targets (for fanout/query/broadcast/pipeline) ──────────────────────
|
|
193
253
|
targets: z
|
|
194
254
|
.array(z.string())
|
|
195
255
|
.optional()
|
|
196
|
-
.describe('Liste des agents cibles (pour fanout/query
|
|
197
|
-
// ─── Pipeline steps ──────────────────────────────────────────────────────
|
|
256
|
+
.describe('Liste des agents cibles (noms ou ports) pour fanout/query/broadcast'),
|
|
198
257
|
steps: z
|
|
199
258
|
.array(z.object({
|
|
200
|
-
agentName: z
|
|
259
|
+
agentName: z
|
|
260
|
+
.string()
|
|
261
|
+
.describe("Nom de l'agent ou port (ex: 'sniperbot_analyst' ou '3001')"),
|
|
201
262
|
promptPrefix: z.string().optional(),
|
|
202
263
|
}))
|
|
203
264
|
.optional()
|
|
204
265
|
.describe('Étapes de la pipeline (pour action=pipeline)'),
|
|
205
|
-
// ─── Options ─────────────────────────────────────────────────────────────
|
|
206
|
-
runner: z.string().optional().default('hermes').describe('Runner à utiliser (default: hermes)'),
|
|
207
|
-
model: z.string().optional().describe('Modèle LLM à utiliser (override)'),
|
|
208
266
|
mergeStrategy: z
|
|
209
267
|
.enum(['concat', 'best', 'vote', 'first_success'])
|
|
210
268
|
.optional()
|
|
@@ -214,232 +272,225 @@ export const a2aHubSchema = z.object({
|
|
|
214
272
|
.boolean()
|
|
215
273
|
.optional()
|
|
216
274
|
.default(false)
|
|
217
|
-
.describe(
|
|
275
|
+
.describe('Pour broadcast: si true, premier qui répond gagne'),
|
|
218
276
|
async: z
|
|
219
277
|
.boolean()
|
|
220
278
|
.optional()
|
|
221
279
|
.default(true)
|
|
222
|
-
.describe('Pour delegate: si true (default), retourne immédiatement
|
|
223
|
-
callbackUrl: z
|
|
224
|
-
.string()
|
|
225
|
-
.optional()
|
|
226
|
-
.describe("Pour delegate: URL appelée quand l'agent termine (POST result)"),
|
|
280
|
+
.describe('Pour delegate: si true (default), retourne immédiatement'),
|
|
227
281
|
accumulateContext: z
|
|
228
282
|
.boolean()
|
|
229
283
|
.optional()
|
|
230
284
|
.default(false)
|
|
231
|
-
.describe('Pour pipeline:
|
|
285
|
+
.describe('Pour pipeline: chaque step reçoit tous les outputs précédents'),
|
|
232
286
|
timeoutMs: z
|
|
233
287
|
.number()
|
|
234
288
|
.int()
|
|
235
|
-
.min(
|
|
236
|
-
.max(
|
|
289
|
+
.min(5000)
|
|
290
|
+
.max(600000)
|
|
237
291
|
.optional()
|
|
238
|
-
.describe('Timeout en ms (
|
|
292
|
+
.describe('Timeout par agent en ms (default: 300000 = 5min)'),
|
|
293
|
+
model: z.string().optional().describe('Modèle LLM override'),
|
|
239
294
|
});
|
|
295
|
+
// ─── Resolve agent name to worker port ─────────────────────────────────────
|
|
296
|
+
function resolveTarget(target, discovery) {
|
|
297
|
+
// If target is a port number
|
|
298
|
+
const portMatch = target.match(/^(\d{4,5})$/);
|
|
299
|
+
if (portMatch) {
|
|
300
|
+
const port = parseInt(portMatch[1], 10);
|
|
301
|
+
const worker = discovery.workers.find((w) => w.port === port);
|
|
302
|
+
return { port, agentName: worker?.agentName || target };
|
|
303
|
+
}
|
|
304
|
+
// If target is an agent name, find its worker
|
|
305
|
+
const agent = discovery.agents.find((a) => a.name === target);
|
|
306
|
+
if (agent?.workerPort) {
|
|
307
|
+
return { port: agent.workerPort, agentName: target };
|
|
308
|
+
}
|
|
309
|
+
// Try online workers
|
|
310
|
+
const worker = discovery.workers.find((w) => w.online && w.agentName === target);
|
|
311
|
+
if (worker) {
|
|
312
|
+
return { port: worker.port, agentName: target };
|
|
313
|
+
}
|
|
314
|
+
return { port: null, agentName: target };
|
|
315
|
+
}
|
|
240
316
|
// ─── Execute ───────────────────────────────────────────────────────────────
|
|
241
317
|
export async function a2aHub(args) {
|
|
242
318
|
const selfAgent = process.env.OVERMIND_AGENT_NAME || 'unknown';
|
|
243
|
-
const
|
|
319
|
+
const timeout = args.timeoutMs ?? 300000;
|
|
244
320
|
try {
|
|
245
|
-
switch (action) {
|
|
321
|
+
switch (args.action) {
|
|
246
322
|
// ═══════════════════════════════════════════════════════════════════════
|
|
247
|
-
// DISCOVER
|
|
323
|
+
// DISCOVER
|
|
248
324
|
// ═══════════════════════════════════════════════════════════════════════
|
|
249
325
|
case 'discover': {
|
|
250
|
-
const
|
|
326
|
+
const d = discoverAgents();
|
|
251
327
|
const lines = [
|
|
252
328
|
`🌐 **A2A Hub — Découverte du système multi-agents**`,
|
|
253
329
|
``,
|
|
254
|
-
`**Self:** ${
|
|
255
|
-
`**
|
|
256
|
-
`**Total agents:** ${
|
|
330
|
+
`**Self:** ${d.selfAgent || '(inconnu)'}`,
|
|
331
|
+
`**Workers online:** ${d.onlineWorkers}`,
|
|
332
|
+
`**Total agents:** ${d.totalAgents}`,
|
|
333
|
+
``,
|
|
334
|
+
`### Workers HTTP`,
|
|
257
335
|
``,
|
|
258
336
|
];
|
|
259
|
-
|
|
337
|
+
const onlineWorkers = d.workers.filter((w) => w.online);
|
|
338
|
+
if (onlineWorkers.length === 0) {
|
|
339
|
+
lines.push('Aucun worker HTTP en ligne.');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
lines.push('| Port | Agent | Status | URL |');
|
|
343
|
+
lines.push('|------|-------|--------|-----|');
|
|
344
|
+
for (const w of onlineWorkers) {
|
|
345
|
+
lines.push(`| :${w.port} | ${w.agentName} | 🟢 online | ${w.url} |`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
lines.push('');
|
|
349
|
+
lines.push('### Agents Hermes');
|
|
350
|
+
lines.push('');
|
|
351
|
+
if (d.agents.length === 0) {
|
|
260
352
|
lines.push('Aucun agent trouvé dans ~/.overmind/hermes/profiles/');
|
|
261
353
|
}
|
|
262
354
|
else {
|
|
263
|
-
lines.push('| Agent | Status | Model | Provider | Skills | Description |');
|
|
264
|
-
lines.push('
|
|
265
|
-
for (const a of
|
|
266
|
-
const statusIcon = a.status === 'online'
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
: a.status === 'idle'
|
|
271
|
-
? '⚪'
|
|
272
|
-
: '🔴';
|
|
273
|
-
lines.push(`| ${a.name === selfAgent ? '**' + a.name + ' (self)**' : a.name} | ${statusIcon} ${a.status} | ${a.model} | ${a.provider} | ${a.skillsCount} | ${a.description.slice(0, 40)} |`);
|
|
355
|
+
lines.push('| Agent | Status | Worker | Model | Provider | Skills | Description |');
|
|
356
|
+
lines.push('|-------|--------|--------|-------|----------|--------|-------------|');
|
|
357
|
+
for (const a of d.agents) {
|
|
358
|
+
const statusIcon = a.status === 'online' ? '🟢' : '🔴';
|
|
359
|
+
const workerInfo = a.workerPort ? `:${a.workerPort}` : '—';
|
|
360
|
+
const nameDisplay = a.name === selfAgent ? `**${a.name} (self)**` : a.name;
|
|
361
|
+
lines.push(`| ${nameDisplay} | ${statusIcon} ${a.status} | ${workerInfo} | ${a.model} | ${a.provider} | ${a.skillsCount} | ${a.description.slice(0, 40)} |`);
|
|
274
362
|
}
|
|
275
|
-
lines.push('');
|
|
276
|
-
lines.push('**Capacités A2A disponibles:**');
|
|
277
|
-
lines.push(' • `send` — Message synchrone A→B');
|
|
278
|
-
lines.push(' • `delegate` — Tâche async + callback');
|
|
279
|
-
lines.push(' • `pipeline` — Chaîne A→B→C');
|
|
280
|
-
lines.push(' • `fanout` — 1→N parallèle + merge');
|
|
281
|
-
lines.push(' • `query` — Question multi-agents');
|
|
282
|
-
lines.push(' • `broadcast` — Message global');
|
|
283
363
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push('**Actions disponibles:** send, delegate, pipeline, fanout, query, broadcast');
|
|
366
|
+
lines.push('**Exemple:** a2a_hub(action: "send", target: "sniperbot_analyst", message: "Analyse BTC")');
|
|
367
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
287
368
|
}
|
|
288
369
|
// ═══════════════════════════════════════════════════════════════════════
|
|
289
|
-
// STATUS
|
|
370
|
+
// STATUS
|
|
290
371
|
// ═══════════════════════════════════════════════════════════════════════
|
|
291
372
|
case 'status': {
|
|
292
373
|
if (!args.target) {
|
|
293
374
|
return {
|
|
294
|
-
content: [
|
|
295
|
-
{
|
|
296
|
-
type: 'text',
|
|
297
|
-
text: '❌ Paramètre `target` requis pour action=status',
|
|
298
|
-
},
|
|
299
|
-
],
|
|
375
|
+
content: [{ type: 'text', text: '❌ `target` requis (nom ou port)' }],
|
|
300
376
|
isError: true,
|
|
301
377
|
};
|
|
302
378
|
}
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
action: 'status',
|
|
307
|
-
});
|
|
308
|
-
const r = result;
|
|
309
|
-
if (r.error) {
|
|
379
|
+
const d = discoverAgents();
|
|
380
|
+
const { port, agentName } = resolveTarget(args.target, d);
|
|
381
|
+
if (!port) {
|
|
310
382
|
return {
|
|
311
383
|
content: [
|
|
312
|
-
{
|
|
313
|
-
type: 'text',
|
|
314
|
-
text: `❌ Erreur status ${args.target}: ${r.error}`,
|
|
315
|
-
},
|
|
384
|
+
{ type: 'text', text: `❌ Worker pour "${args.target}" introuvable` },
|
|
316
385
|
],
|
|
317
386
|
isError: true,
|
|
318
387
|
};
|
|
319
388
|
}
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
lines.push(`**Erreurs:** ${local.totalErrors || 0}`);
|
|
327
|
-
lines.push(`**A2A reçus:** ${local.a2aReceived || 0}`);
|
|
328
|
-
lines.push(`**A2A envoyés:** ${local.a2aSent || 0}`);
|
|
329
|
-
if (local.currentSessionId) {
|
|
330
|
-
lines.push(`**Session:** ${local.currentSessionId}`);
|
|
331
|
-
}
|
|
332
|
-
if (local.lastActivityAt) {
|
|
333
|
-
lines.push(`**Dernière activité:** ${new Date(local.lastActivityAt).toISOString()}`);
|
|
334
|
-
}
|
|
389
|
+
const health = httpGet(`http://localhost:${port}/health`);
|
|
390
|
+
if (!health) {
|
|
391
|
+
return {
|
|
392
|
+
content: [{ type: 'text', text: `❌ Worker :${port} injoignable` }],
|
|
393
|
+
isError: true,
|
|
394
|
+
};
|
|
335
395
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
396
|
+
const lines = [`📊 **Status: ${agentName} (:${port})**`, ``];
|
|
397
|
+
for (const [key, value] of Object.entries(health)) {
|
|
398
|
+
lines.push(`**${key}:** ${typeof value === 'object' ? JSON.stringify(value).slice(0, 100) : value}`);
|
|
399
|
+
}
|
|
400
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
339
401
|
}
|
|
340
402
|
// ═══════════════════════════════════════════════════════════════════════
|
|
341
|
-
// SEND
|
|
403
|
+
// SEND
|
|
342
404
|
// ═══════════════════════════════════════════════════════════════════════
|
|
343
405
|
case 'send': {
|
|
344
406
|
if (!args.target || !args.message) {
|
|
407
|
+
return {
|
|
408
|
+
content: [{ type: 'text', text: '❌ `target` et `message` requis' }],
|
|
409
|
+
isError: true,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const d = discoverAgents();
|
|
413
|
+
const { port, agentName } = resolveTarget(args.target, d);
|
|
414
|
+
if (!port) {
|
|
345
415
|
return {
|
|
346
416
|
content: [
|
|
347
417
|
{
|
|
348
418
|
type: 'text',
|
|
349
|
-
text:
|
|
419
|
+
text: `❌ Worker pour "${args.target}" introuvable. Utilisez action=discover pour lister les agents.`,
|
|
350
420
|
},
|
|
351
421
|
],
|
|
352
422
|
isError: true,
|
|
353
423
|
};
|
|
354
424
|
}
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
prompt: args.message,
|
|
360
|
-
...(args.model ? { model: args.model } : {}),
|
|
425
|
+
const enrichedMessage = `[A2A — Message from ${selfAgent}]\n${args.message}`;
|
|
426
|
+
const result = sendToWorker(port, enrichedMessage, {
|
|
427
|
+
model: args.model,
|
|
428
|
+
timeoutMs: timeout,
|
|
361
429
|
});
|
|
362
|
-
|
|
363
|
-
if (r.error) {
|
|
430
|
+
if (!result.success) {
|
|
364
431
|
return {
|
|
365
|
-
content: [
|
|
366
|
-
{
|
|
367
|
-
type: 'text',
|
|
368
|
-
text: `❌ Erreur send → ${args.target}: ${r.error}`,
|
|
369
|
-
},
|
|
370
|
-
],
|
|
432
|
+
content: [{ type: 'text', text: `❌ ${result.text}` }],
|
|
371
433
|
isError: true,
|
|
372
434
|
};
|
|
373
435
|
}
|
|
374
|
-
const content = (r.content || []);
|
|
375
|
-
const responseText = content.map((c) => c.text).join('\n');
|
|
376
436
|
return {
|
|
377
437
|
content: [
|
|
378
438
|
{
|
|
379
439
|
type: 'text',
|
|
380
|
-
text: `📤 **Message envoyé à ${
|
|
440
|
+
text: `📤 **Message envoyé à ${agentName} (:${port})**\n\n${result.text}`,
|
|
381
441
|
},
|
|
382
442
|
],
|
|
383
443
|
};
|
|
384
444
|
}
|
|
385
445
|
// ═══════════════════════════════════════════════════════════════════════
|
|
386
|
-
// DELEGATE —
|
|
446
|
+
// DELEGATE (async — fire and forget)
|
|
387
447
|
// ═══════════════════════════════════════════════════════════════════════
|
|
388
448
|
case 'delegate': {
|
|
389
449
|
if (!args.target || !args.message) {
|
|
390
450
|
return {
|
|
391
|
-
content: [
|
|
392
|
-
{
|
|
393
|
-
type: 'text',
|
|
394
|
-
text: '❌ Paramètres `target` et `message` requis pour action=delegate',
|
|
395
|
-
},
|
|
396
|
-
],
|
|
451
|
+
content: [{ type: 'text', text: '❌ `target` et `message` requis' }],
|
|
397
452
|
isError: true,
|
|
398
453
|
};
|
|
399
454
|
}
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
runner: args.runner,
|
|
404
|
-
prompt: args.message,
|
|
405
|
-
async: args.async,
|
|
406
|
-
...(args.model ? { model: args.model } : {}),
|
|
407
|
-
...(args.callbackUrl ? { callbackUrl: args.callbackUrl } : {}),
|
|
408
|
-
});
|
|
409
|
-
const r = result;
|
|
410
|
-
if (r.error) {
|
|
455
|
+
const d = discoverAgents();
|
|
456
|
+
const { port, agentName } = resolveTarget(args.target, d);
|
|
457
|
+
if (!port) {
|
|
411
458
|
return {
|
|
412
459
|
content: [
|
|
413
|
-
{
|
|
414
|
-
type: 'text',
|
|
415
|
-
text: `❌ Erreur delegate → ${args.target}: ${r.error}`,
|
|
416
|
-
},
|
|
460
|
+
{ type: 'text', text: `❌ Worker pour "${args.target}" introuvable` },
|
|
417
461
|
],
|
|
418
462
|
isError: true,
|
|
419
463
|
};
|
|
420
464
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
465
|
+
// Fire and forget — don't wait for response
|
|
466
|
+
const enrichedMessage = `[A2A — Delegate from ${selfAgent}]\n${args.message}`;
|
|
467
|
+
const url = `http://localhost:${port}/send`;
|
|
468
|
+
const body = {
|
|
469
|
+
message: enrichedMessage,
|
|
470
|
+
userId: `a2a_${selfAgent}`,
|
|
471
|
+
username: selfAgent,
|
|
472
|
+
channelId: 'a2a_delegate',
|
|
473
|
+
...(args.model ? { model: args.model } : {}),
|
|
474
|
+
};
|
|
475
|
+
// Spawn curl in background (fire-and-forget)
|
|
476
|
+
try {
|
|
477
|
+
const payload = JSON.stringify(body).replace(/'/g, "'\\''");
|
|
478
|
+
execSync(`nohup curl -s -m ${Math.floor(timeout / 1000)} -X POST "${url}" -H "Content-Type: application/json" -d '${payload}' > /dev/null 2>&1 &`, { timeout: 2000 });
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// ignore — fire and forget
|
|
430
482
|
}
|
|
431
|
-
const content = (r.content || []);
|
|
432
483
|
return {
|
|
433
484
|
content: [
|
|
434
485
|
{
|
|
435
486
|
type: 'text',
|
|
436
|
-
text:
|
|
487
|
+
text: `🤝 **Tâche déléguée à ${agentName} (:${port})**\n\nLe worker traite la requête en arrière-plan. Le résultat sera disponible dans sa session.`,
|
|
437
488
|
},
|
|
438
489
|
],
|
|
439
490
|
};
|
|
440
491
|
}
|
|
441
492
|
// ═══════════════════════════════════════════════════════════════════════
|
|
442
|
-
// PIPELINE
|
|
493
|
+
// PIPELINE
|
|
443
494
|
// ═══════════════════════════════════════════════════════════════════════
|
|
444
495
|
case 'pipeline': {
|
|
445
496
|
if (!args.message || !args.steps || args.steps.length === 0) {
|
|
@@ -447,201 +498,227 @@ export async function a2aHub(args) {
|
|
|
447
498
|
content: [
|
|
448
499
|
{
|
|
449
500
|
type: 'text',
|
|
450
|
-
text:
|
|
501
|
+
text: '❌ `message` (prompt initial) et `steps` requis pour pipeline',
|
|
451
502
|
},
|
|
452
503
|
],
|
|
453
504
|
isError: true,
|
|
454
505
|
};
|
|
455
506
|
}
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
507
|
+
const d = discoverAgents();
|
|
508
|
+
const outputs = [];
|
|
509
|
+
let currentPrompt = args.message;
|
|
510
|
+
for (let i = 0; i < args.steps.length; i++) {
|
|
511
|
+
const step = args.steps[i];
|
|
512
|
+
const { port, agentName } = resolveTarget(step.agentName, d);
|
|
513
|
+
if (!port) {
|
|
514
|
+
outputs.push({ agent: step.agentName, output: 'Worker introuvable', success: false });
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
const stepPrompt = (step.promptPrefix ? step.promptPrefix + '\n\n' : '') +
|
|
518
|
+
`[Pipeline Step ${i + 1}/${args.steps.length}]\n${currentPrompt}`;
|
|
519
|
+
const result = sendToWorker(port, stepPrompt, { timeoutMs: timeout });
|
|
520
|
+
outputs.push({
|
|
521
|
+
agent: `${agentName} (:${port})`,
|
|
522
|
+
output: result.text,
|
|
523
|
+
success: result.success,
|
|
524
|
+
});
|
|
525
|
+
if (!result.success)
|
|
526
|
+
break;
|
|
527
|
+
currentPrompt = args.accumulateContext
|
|
528
|
+
? outputs.map((o) => `[${o.agent}]: ${o.output}`).join('\n\n---\n\n')
|
|
529
|
+
: result.text;
|
|
475
530
|
}
|
|
476
|
-
const outputs = (r.outputs || []);
|
|
477
531
|
const lines = [
|
|
478
|
-
`🔗 **Pipeline terminé** (${
|
|
532
|
+
`🔗 **Pipeline terminé** (${outputs.length}/${args.steps.length} steps)`,
|
|
479
533
|
``,
|
|
480
534
|
];
|
|
481
535
|
for (const o of outputs) {
|
|
482
|
-
lines.push(`**${o.success ? '✅' : '❌'} ${o.
|
|
483
|
-
lines.push(o.output.slice(0,
|
|
536
|
+
lines.push(`**${o.success ? '✅' : '❌'} ${o.agent}:**`);
|
|
537
|
+
lines.push(o.output.slice(0, 800) + (o.output.length > 800 ? '...' : ''));
|
|
484
538
|
lines.push('');
|
|
485
539
|
}
|
|
486
|
-
lines.
|
|
487
|
-
lines.push((r.finalOutput || '').slice(0, 1000));
|
|
488
|
-
return {
|
|
489
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
490
|
-
};
|
|
540
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
491
541
|
}
|
|
492
542
|
// ═══════════════════════════════════════════════════════════════════════
|
|
493
|
-
// FANOUT
|
|
543
|
+
// FANOUT
|
|
494
544
|
// ═══════════════════════════════════════════════════════════════════════
|
|
495
545
|
case 'fanout': {
|
|
496
546
|
if (!args.message || !args.targets || args.targets.length === 0) {
|
|
497
547
|
return {
|
|
498
|
-
content: [
|
|
499
|
-
{
|
|
500
|
-
type: 'text',
|
|
501
|
-
text: '❌ Paramètres `message` et `targets` requis pour action=fanout',
|
|
502
|
-
},
|
|
503
|
-
],
|
|
548
|
+
content: [{ type: 'text', text: '❌ `message` et `targets` requis' }],
|
|
504
549
|
isError: true,
|
|
505
550
|
};
|
|
506
551
|
}
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
552
|
+
const d = discoverAgents();
|
|
553
|
+
const enrichedMessage = `[A2A — Fanout from ${selfAgent}]\n${args.message}`;
|
|
554
|
+
// Run all in parallel
|
|
555
|
+
const results = args.targets
|
|
556
|
+
.map((target) => {
|
|
557
|
+
const { port } = resolveTarget(target, d);
|
|
558
|
+
if (!port) {
|
|
559
|
+
return { agent: target, success: false, text: 'Worker introuvable' };
|
|
560
|
+
}
|
|
561
|
+
return sendToWorker(port, enrichedMessage, { model: args.model, timeoutMs: timeout });
|
|
562
|
+
})
|
|
563
|
+
.map((r, i) => ({
|
|
564
|
+
agent: args.targets[i],
|
|
565
|
+
success: r.success,
|
|
566
|
+
text: r.text,
|
|
567
|
+
}));
|
|
568
|
+
// Merge
|
|
569
|
+
let merged;
|
|
570
|
+
let winner;
|
|
571
|
+
switch (args.mergeStrategy) {
|
|
572
|
+
case 'first_success': {
|
|
573
|
+
const first = results.find((r) => r.success);
|
|
574
|
+
merged = first ? first.text : 'Tous ont échoué';
|
|
575
|
+
winner = first?.agent;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case 'best': {
|
|
579
|
+
const sorted = results
|
|
580
|
+
.filter((r) => r.success)
|
|
581
|
+
.sort((a, b) => b.text.length - a.text.length);
|
|
582
|
+
merged = sorted.length > 0 ? sorted[0].text : 'Tous ont échoué';
|
|
583
|
+
winner = sorted[0]?.agent;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
case 'concat':
|
|
587
|
+
default: {
|
|
588
|
+
merged = results
|
|
589
|
+
.map((r) => `### ${r.agent}${r.success ? '' : ' (ÉCHEC)'}\n${r.text}`)
|
|
590
|
+
.join('\n\n---\n\n');
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
527
593
|
}
|
|
594
|
+
const successCount = results.filter((r) => r.success).length;
|
|
528
595
|
const lines = [
|
|
529
|
-
`🌐 **Fanout
|
|
530
|
-
...(
|
|
596
|
+
`🌐 **Fanout** (${successCount}/${results.length} succès, merge=${args.mergeStrategy})`,
|
|
597
|
+
...(winner ? [`**Gagnant:** ${winner}`] : []),
|
|
531
598
|
``,
|
|
532
|
-
|
|
533
|
-
(r.merged || '').slice(0, 2000),
|
|
599
|
+
merged.slice(0, 3000),
|
|
534
600
|
];
|
|
535
|
-
return {
|
|
536
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
537
|
-
};
|
|
601
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
538
602
|
}
|
|
539
603
|
// ═══════════════════════════════════════════════════════════════════════
|
|
540
|
-
// QUERY
|
|
604
|
+
// QUERY
|
|
541
605
|
// ═══════════════════════════════════════════════════════════════════════
|
|
542
606
|
case 'query': {
|
|
543
607
|
if (!args.message || !args.targets || args.targets.length === 0) {
|
|
544
608
|
return {
|
|
545
|
-
content: [
|
|
546
|
-
{
|
|
547
|
-
type: 'text',
|
|
548
|
-
text: '❌ Paramètres `message` et `targets` requis pour action=query',
|
|
549
|
-
},
|
|
550
|
-
],
|
|
609
|
+
content: [{ type: 'text', text: '❌ `message` et `targets` requis' }],
|
|
551
610
|
isError: true,
|
|
552
611
|
};
|
|
553
612
|
}
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
const results = (r.results || []);
|
|
613
|
+
const d = discoverAgents();
|
|
614
|
+
const enrichedMessage = `[A2A — Query from ${selfAgent}]\n${args.message}`;
|
|
615
|
+
const queryTimeout = Math.min(timeout, 60000);
|
|
616
|
+
const results = args.targets
|
|
617
|
+
.map((target) => {
|
|
618
|
+
const { port } = resolveTarget(target, d);
|
|
619
|
+
if (!port) {
|
|
620
|
+
return { agent: target, success: false, text: 'Worker introuvable' };
|
|
621
|
+
}
|
|
622
|
+
return sendToWorker(port, enrichedMessage, {
|
|
623
|
+
model: args.model,
|
|
624
|
+
timeoutMs: queryTimeout,
|
|
625
|
+
});
|
|
626
|
+
})
|
|
627
|
+
.map((r, i) => ({
|
|
628
|
+
agent: args.targets[i],
|
|
629
|
+
success: r.success,
|
|
630
|
+
text: r.text,
|
|
631
|
+
}));
|
|
575
632
|
const lines = [
|
|
576
|
-
`❓ **Query multi-agents** (${r.
|
|
633
|
+
`❓ **Query multi-agents** (${results.filter((r) => r.success).length}/${results.length} réponses)`,
|
|
577
634
|
``,
|
|
578
635
|
];
|
|
579
|
-
for (const
|
|
580
|
-
lines.push(`**${
|
|
581
|
-
lines.push(
|
|
636
|
+
for (const r of results) {
|
|
637
|
+
lines.push(`**${r.success ? '✅' : '❌'} ${r.agent}:**`);
|
|
638
|
+
lines.push(r.text.slice(0, 500) + (r.text.length > 500 ? '...' : ''));
|
|
582
639
|
lines.push('');
|
|
583
640
|
}
|
|
584
|
-
return {
|
|
585
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
586
|
-
};
|
|
641
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
587
642
|
}
|
|
588
643
|
// ═══════════════════════════════════════════════════════════════════════
|
|
589
|
-
// BROADCAST
|
|
644
|
+
// BROADCAST
|
|
590
645
|
// ═══════════════════════════════════════════════════════════════════════
|
|
591
646
|
case 'broadcast': {
|
|
592
647
|
if (!args.message) {
|
|
593
648
|
return {
|
|
594
|
-
content: [
|
|
595
|
-
{
|
|
596
|
-
type: 'text',
|
|
597
|
-
text: '❌ Paramètre `message` requis pour action=broadcast',
|
|
598
|
-
},
|
|
599
|
-
],
|
|
649
|
+
content: [{ type: 'text', text: '❌ `message` requis' }],
|
|
600
650
|
isError: true,
|
|
601
651
|
};
|
|
602
652
|
}
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
653
|
+
const d = discoverAgents();
|
|
654
|
+
// Determine targets
|
|
655
|
+
let targetWorkers;
|
|
656
|
+
if (args.targets && args.targets.length > 0) {
|
|
657
|
+
// Resolve each target to a worker
|
|
658
|
+
targetWorkers = args.targets
|
|
659
|
+
.map((t) => {
|
|
660
|
+
const { port } = resolveTarget(t, d);
|
|
661
|
+
return d.workers.find((w) => w.port === port);
|
|
662
|
+
})
|
|
663
|
+
.filter((w) => w !== undefined);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
// All online workers
|
|
667
|
+
targetWorkers = d.workers.filter((w) => w.online);
|
|
668
|
+
}
|
|
669
|
+
if (targetWorkers.length === 0) {
|
|
614
670
|
return {
|
|
615
|
-
content: [
|
|
616
|
-
{
|
|
617
|
-
type: 'text',
|
|
618
|
-
text: `❌ Erreur broadcast: ${r.error}`,
|
|
619
|
-
},
|
|
620
|
-
],
|
|
671
|
+
content: [{ type: 'text', text: '❌ Aucun worker online pour broadcast' }],
|
|
621
672
|
isError: true,
|
|
622
673
|
};
|
|
623
674
|
}
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
675
|
+
const enrichedMessage = `[A2A — Broadcast from ${selfAgent}]\n${args.message}`;
|
|
676
|
+
if (args.race) {
|
|
677
|
+
// First to respond wins
|
|
678
|
+
for (const w of targetWorkers) {
|
|
679
|
+
const result = sendToWorker(w.port, enrichedMessage, {
|
|
680
|
+
model: args.model,
|
|
681
|
+
timeoutMs: timeout,
|
|
682
|
+
});
|
|
683
|
+
if (result.success) {
|
|
684
|
+
return {
|
|
685
|
+
content: [
|
|
686
|
+
{
|
|
687
|
+
type: 'text',
|
|
688
|
+
text: `📡 **Broadcast race — ${w.agentName} (:${w.port}) a gagné!**\n\n${result.text}`,
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
content: [{ type: 'text', text: '❌ Tous les workers ont échoué' }],
|
|
696
|
+
isError: true,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
// Send to all in parallel
|
|
700
|
+
const results = targetWorkers.map((w) => {
|
|
701
|
+
const r = sendToWorker(w.port, enrichedMessage, {
|
|
702
|
+
model: args.model,
|
|
703
|
+
timeoutMs: timeout,
|
|
704
|
+
});
|
|
705
|
+
return { agent: `${w.agentName} (:${w.port})`, success: r.success, text: r.text };
|
|
706
|
+
});
|
|
707
|
+
const successCount = results.filter((r) => r.success).length;
|
|
708
|
+
const lines = [`📡 **Broadcast** — ${successCount}/${results.length} succès`, ``];
|
|
709
|
+
for (const r of results) {
|
|
710
|
+
lines.push(`**${r.success ? '✅' : '❌'} ${r.agent}:**`);
|
|
711
|
+
lines.push(r.text.slice(0, 300) + (r.text.length > 300 ? '...' : ''));
|
|
633
712
|
lines.push('');
|
|
634
713
|
}
|
|
635
|
-
return {
|
|
636
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
637
|
-
};
|
|
714
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
638
715
|
}
|
|
639
716
|
default:
|
|
640
717
|
return {
|
|
641
718
|
content: [
|
|
642
719
|
{
|
|
643
720
|
type: 'text',
|
|
644
|
-
text: `❌ Action inconnue: ${action}
|
|
721
|
+
text: `❌ Action inconnue: ${args.action}`,
|
|
645
722
|
},
|
|
646
723
|
],
|
|
647
724
|
isError: true,
|