overmind-mcp 2.8.11 → 2.8.13
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/dist/bridge/AgentRegistry.d.ts +123 -0
- package/dist/bridge/AgentRegistry.d.ts.map +1 -0
- package/dist/bridge/AgentRegistry.js +207 -0
- package/dist/bridge/AgentRegistry.js.map +1 -0
- package/dist/bridge/MessageLog.d.ts +142 -0
- package/dist/bridge/MessageLog.d.ts.map +1 -0
- package/dist/bridge/MessageLog.js +311 -0
- package/dist/bridge/MessageLog.js.map +1 -0
- package/dist/bridge/OverBridgeServer.d.ts +145 -0
- package/dist/bridge/OverBridgeServer.d.ts.map +1 -0
- package/dist/bridge/OverBridgeServer.js +654 -0
- package/dist/bridge/OverBridgeServer.js.map +1 -0
- package/dist/bridge/index.d.ts +7 -1
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +9 -1
- package/dist/bridge/index.js.map +1 -1
- package/dist/services/AgentManager.d.ts.map +1 -1
- package/dist/services/AgentManager.js +16 -3
- package/dist/services/AgentManager.js.map +1 -1
- package/dist/tools/create_agent.d.ts.map +1 -1
- package/dist/tools/create_agent.js +11 -0
- package/dist/tools/create_agent.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔══════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ OVERMIND BRIDGE — OverBridgeServer (HTTP JSON-RPC 2.0 Entry Point)║
|
|
4
|
+
* ║ ║
|
|
5
|
+
* ║ Serveur HTTP qui expose l'API Overmind Bridge via JSON-RPC 2.0. ║
|
|
6
|
+
* ║ Permet à n'importe quel client (curl, Python, fetch...) de : ║
|
|
7
|
+
* ║ - Parler à un agent ║
|
|
8
|
+
* ║ - Faire parler des agents entre eux (A2A) ║
|
|
9
|
+
* ║ - Suivre l'état live des agents (busy/idle/online) ║
|
|
10
|
+
* ║ - Consulter l'historique persistant des messages ║
|
|
11
|
+
* ║ ║
|
|
12
|
+
* ║ ARCHITECTURE ║
|
|
13
|
+
* ║ ───────────── ║
|
|
14
|
+
* ║ Client HTTP → POST /rpc → JSON-RPC dispatcher ║
|
|
15
|
+
* ║ → OverBridgeService (deja wrappe MCP) ║
|
|
16
|
+
* ║ → AgentRegistry (etat live) ║
|
|
17
|
+
* ║ → MessageLog (persistence Postgres) ║
|
|
18
|
+
* ║ → Overmind MCP :3099 ║
|
|
19
|
+
* ╚══════════════════════════════════════════════════════════════════════╝
|
|
20
|
+
*/
|
|
21
|
+
import http from 'node:http';
|
|
22
|
+
import { URL } from 'node:url';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { AgentRegistry } from './AgentRegistry.js';
|
|
25
|
+
import { MessageLog } from './MessageLog.js';
|
|
26
|
+
import { createBridgeLogger, validateAgentName } from './utils.js';
|
|
27
|
+
const JSON_RPC_ERRORS = {
|
|
28
|
+
PARSE_ERROR: { code: -32700, message: 'Parse error' },
|
|
29
|
+
INVALID_REQUEST: { code: -32600, message: 'Invalid Request' },
|
|
30
|
+
METHOD_NOT_FOUND: { code: -32601, message: 'Method not found' },
|
|
31
|
+
INVALID_PARAMS: { code: -32602, message: 'Invalid params' },
|
|
32
|
+
INTERNAL_ERROR: { code: -32603, message: 'Internal error' },
|
|
33
|
+
AGENT_BUSY: { code: -32001, message: 'Agent is busy' },
|
|
34
|
+
AGENT_OFFLINE: { code: -32002, message: 'Agent is offline' },
|
|
35
|
+
};
|
|
36
|
+
// ─── Zod Schemas (validation des params par méthode) ─────────────────────
|
|
37
|
+
const RunAgentParams = z.object({
|
|
38
|
+
agentName: z.string().min(1),
|
|
39
|
+
runner: z.string().min(1),
|
|
40
|
+
prompt: z.string().min(1),
|
|
41
|
+
sessionId: z.string().optional(),
|
|
42
|
+
path: z.string().optional(),
|
|
43
|
+
model: z.string().optional(),
|
|
44
|
+
mode: z.string().optional(),
|
|
45
|
+
silent: z.boolean().optional(),
|
|
46
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
47
|
+
});
|
|
48
|
+
const A2AParams = z.object({
|
|
49
|
+
fromAgent: z.string().min(1),
|
|
50
|
+
toAgent: z.string().min(1),
|
|
51
|
+
runner: z.string().min(1),
|
|
52
|
+
prompt: z.string().min(1),
|
|
53
|
+
model: z.string().optional(),
|
|
54
|
+
path: z.string().optional(),
|
|
55
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
56
|
+
});
|
|
57
|
+
const AgentStatusParams = z.object({
|
|
58
|
+
agentName: z.string().min(1),
|
|
59
|
+
runner: z.string().optional(),
|
|
60
|
+
action: z.enum(['status', 'stream', 'kill', 'wait']).default('status'),
|
|
61
|
+
sinceTimestamp: z.number().optional(),
|
|
62
|
+
timeoutMs: z.number().optional(),
|
|
63
|
+
});
|
|
64
|
+
const ListAgentsParams = z.object({
|
|
65
|
+
status: z.enum(['online', 'offline', 'busy', 'idle']).optional(),
|
|
66
|
+
runner: z.string().optional(),
|
|
67
|
+
}).default({});
|
|
68
|
+
const MessageHistoryParams = z.object({
|
|
69
|
+
toAgent: z.string().optional(),
|
|
70
|
+
fromAgent: z.string().nullable().optional(),
|
|
71
|
+
status: z.enum(['pending', 'running', 'done', 'failed', 'timeout']).optional(),
|
|
72
|
+
limit: z.number().int().min(1).max(500).default(50),
|
|
73
|
+
offset: z.number().int().min(0).default(0),
|
|
74
|
+
sinceHours: z.number().positive().optional(),
|
|
75
|
+
});
|
|
76
|
+
const MessageGetParams = z.object({
|
|
77
|
+
id: z.string().uuid(),
|
|
78
|
+
});
|
|
79
|
+
const MessageReplayParams = z.object({
|
|
80
|
+
id: z.string().uuid(),
|
|
81
|
+
});
|
|
82
|
+
// ─── OverBridgeServer ─────────────────────────────────────────────────────
|
|
83
|
+
export class OverBridgeServer {
|
|
84
|
+
service;
|
|
85
|
+
registry;
|
|
86
|
+
log;
|
|
87
|
+
messageLog;
|
|
88
|
+
config;
|
|
89
|
+
server;
|
|
90
|
+
startTime = 0;
|
|
91
|
+
constructor(service, config, logger) {
|
|
92
|
+
this.service = service;
|
|
93
|
+
this.config = config;
|
|
94
|
+
this.log = logger ?? createBridgeLogger('overbridge-server');
|
|
95
|
+
this.registry = new AgentRegistry(this.log);
|
|
96
|
+
if (config.enableMessageLog) {
|
|
97
|
+
this.messageLog = new MessageLog(config.postgres, this.log);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Démarre le serveur HTTP et initialise les dépendances (MessageLog, OverBridgeService).
|
|
103
|
+
*/
|
|
104
|
+
async start() {
|
|
105
|
+
this.startTime = Date.now();
|
|
106
|
+
// 1) Init MessageLog (Postgres)
|
|
107
|
+
if (this.messageLog) {
|
|
108
|
+
await this.messageLog.init();
|
|
109
|
+
}
|
|
110
|
+
// 2) Connect OverBridgeService (MCP healthcheck)
|
|
111
|
+
try {
|
|
112
|
+
const status = await this.service.connect(this.config.healthCheckIntervalMs);
|
|
113
|
+
this.log.info(`🔌 OverBridgeService connected: ${status.status}`);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
this.log.warn(`⚠️ OverBridgeService connect failed: ${err.message}`);
|
|
117
|
+
// On continue quand même, le serveur répondra avec erreurs si MCP down
|
|
118
|
+
}
|
|
119
|
+
// 3) Démarre le serveur HTTP
|
|
120
|
+
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
121
|
+
await new Promise((resolve) => {
|
|
122
|
+
this.server.listen(this.config.port, this.config.host, () => resolve());
|
|
123
|
+
});
|
|
124
|
+
const addr = this.server.address();
|
|
125
|
+
const port = typeof addr === 'object' && addr ? addr.port : this.config.port;
|
|
126
|
+
const url = `http://${this.config.host}:${port}`;
|
|
127
|
+
this.log.info(`🚀 OverBridgeServer listening on ${url}`);
|
|
128
|
+
this.log.info(` POST ${url}/rpc (JSON-RPC 2.0)`);
|
|
129
|
+
this.log.info(` GET ${url}/health`);
|
|
130
|
+
return { port, host: this.config.host, url };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Ferme proprement le serveur HTTP et les dépendances.
|
|
134
|
+
*/
|
|
135
|
+
async stop() {
|
|
136
|
+
if (this.server) {
|
|
137
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
138
|
+
this.server = undefined;
|
|
139
|
+
this.log.info('🛑 HTTP server closed');
|
|
140
|
+
}
|
|
141
|
+
this.service.disconnect();
|
|
142
|
+
if (this.messageLog) {
|
|
143
|
+
await this.messageLog.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Expose le registry (pour tests / inspection).
|
|
148
|
+
*/
|
|
149
|
+
get agentRegistry() {
|
|
150
|
+
return this.registry;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Expose le MessageLog (pour tests / inspection).
|
|
154
|
+
*/
|
|
155
|
+
get messages() {
|
|
156
|
+
return this.messageLog;
|
|
157
|
+
}
|
|
158
|
+
// ─── HTTP Request Handler ───────────────────────────────────────────────
|
|
159
|
+
async handleRequest(req, res) {
|
|
160
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
161
|
+
// CORS preflight
|
|
162
|
+
if (req.method === 'OPTIONS') {
|
|
163
|
+
this.writeCors(res);
|
|
164
|
+
res.writeHead(204);
|
|
165
|
+
res.end();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.writeCors(res);
|
|
169
|
+
try {
|
|
170
|
+
// Health check simple
|
|
171
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
172
|
+
await this.handleHealth(res);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// JSON-RPC endpoint
|
|
176
|
+
if (req.method === 'POST' && url.pathname === '/rpc') {
|
|
177
|
+
await this.handleRpc(req, res);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// 404
|
|
181
|
+
this.writeJson(res, 404, { error: 'Not found', path: url.pathname });
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
this.log.error(`💥 Unhandled error: ${err.message}`);
|
|
185
|
+
this.writeJson(res, 500, { error: 'Internal server error' });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
writeCors(res) {
|
|
189
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
190
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
191
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
192
|
+
}
|
|
193
|
+
writeJson(res, status, body) {
|
|
194
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
195
|
+
res.end(JSON.stringify(body));
|
|
196
|
+
}
|
|
197
|
+
// ─── /health Endpoint ───────────────────────────────────────────────────
|
|
198
|
+
async handleHealth(res) {
|
|
199
|
+
const mcpHealth = await this.service.proxyAccess.healthCheck();
|
|
200
|
+
const regStats = this.registry.stats();
|
|
201
|
+
let msgStats = null;
|
|
202
|
+
if (this.messageLog) {
|
|
203
|
+
try {
|
|
204
|
+
msgStats = await this.messageLog.stats();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
msgStats = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.writeJson(res, 200, {
|
|
211
|
+
status: mcpHealth.status,
|
|
212
|
+
uptime: Date.now() - this.startTime,
|
|
213
|
+
mcp: mcpHealth,
|
|
214
|
+
agents: regStats,
|
|
215
|
+
messages: msgStats,
|
|
216
|
+
version: '1.0.0',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// ─── /rpc Endpoint (JSON-RPC 2.0 Dispatcher) ────────────────────────────
|
|
220
|
+
async handleRpc(req, res) {
|
|
221
|
+
// Auth check
|
|
222
|
+
if (this.config.authToken) {
|
|
223
|
+
const auth = req.headers.authorization;
|
|
224
|
+
if (auth !== `Bearer ${this.config.authToken}`) {
|
|
225
|
+
this.writeJson(res, 401, {
|
|
226
|
+
jsonrpc: '2.0',
|
|
227
|
+
id: null,
|
|
228
|
+
error: { code: -32000, message: 'Unauthorized' },
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Body parsing
|
|
234
|
+
const raw = await this.readBody(req);
|
|
235
|
+
let parsed;
|
|
236
|
+
try {
|
|
237
|
+
parsed = JSON.parse(raw);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Batch ou single
|
|
244
|
+
if (Array.isArray(parsed)) {
|
|
245
|
+
if (parsed.length === 0) {
|
|
246
|
+
this.respondError(res, null, JSON_RPC_ERRORS.INVALID_REQUEST);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const responses = await Promise.all(parsed.map((r) => this.dispatchRpc(r).catch((e) => this.buildErrorResponse(r.id ?? null, e))));
|
|
250
|
+
this.writeJson(res, 200, responses);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const response = await this.dispatchRpc(parsed);
|
|
254
|
+
this.writeJson(res, 200, response);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Dispatch une requête JSON-RPC vers la bonne méthode.
|
|
259
|
+
*/
|
|
260
|
+
async dispatchRpc(req) {
|
|
261
|
+
// Validation de base JSON-RPC 2.0
|
|
262
|
+
if (req.jsonrpc !== '2.0' || !req.method) {
|
|
263
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: JSON_RPC_ERRORS.INVALID_REQUEST };
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
switch (req.method) {
|
|
267
|
+
case 'agent.run':
|
|
268
|
+
return this.methodAgentRun(req);
|
|
269
|
+
case 'agent.a2a':
|
|
270
|
+
return this.methodAgentA2A(req);
|
|
271
|
+
case 'agent.status':
|
|
272
|
+
return this.methodAgentStatus(req);
|
|
273
|
+
case 'agent.list':
|
|
274
|
+
return this.methodAgentList(req);
|
|
275
|
+
case 'agent.kill':
|
|
276
|
+
return this.methodAgentKill(req);
|
|
277
|
+
case 'message.history':
|
|
278
|
+
return this.methodMessageHistory(req);
|
|
279
|
+
case 'message.get':
|
|
280
|
+
return this.methodMessageGet(req);
|
|
281
|
+
case 'message.replay':
|
|
282
|
+
return this.methodMessageReplay(req);
|
|
283
|
+
case 'message.stats':
|
|
284
|
+
return this.methodMessageStats(req);
|
|
285
|
+
case 'health.ping':
|
|
286
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { pong: true, ts: Date.now() } };
|
|
287
|
+
default:
|
|
288
|
+
return {
|
|
289
|
+
jsonrpc: '2.0',
|
|
290
|
+
id: req.id ?? null,
|
|
291
|
+
error: { ...JSON_RPC_ERRORS.METHOD_NOT_FOUND, data: { method: req.method } },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
this.log.error(`💥 ${req.method} failed: ${err.message}`);
|
|
297
|
+
return this.buildErrorResponse(req.id ?? null, err);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// ─── RPC Methods ─────────────────────────────────────────────────────────
|
|
301
|
+
/**
|
|
302
|
+
* agent.run — Lance un agent (from client externe OU from autre agent).
|
|
303
|
+
*/
|
|
304
|
+
async methodAgentRun(req) {
|
|
305
|
+
const params = this.validateParams(RunAgentParams, req.params, req.id);
|
|
306
|
+
if (!params.ok)
|
|
307
|
+
return params.response;
|
|
308
|
+
const { agentName, runner, prompt, sessionId, path, model, mode, silent, metadata } = params.data;
|
|
309
|
+
const validatedAgentName = validateAgentName(agentName);
|
|
310
|
+
// Register
|
|
311
|
+
this.registry.register(validatedAgentName, runner);
|
|
312
|
+
// Persist (pending)
|
|
313
|
+
let messageId;
|
|
314
|
+
if (this.messageLog) {
|
|
315
|
+
messageId = await this.messageLog.create({
|
|
316
|
+
fromAgent: metadata?.fromAgent ?? null,
|
|
317
|
+
toAgent: validatedAgentName,
|
|
318
|
+
runner: runner,
|
|
319
|
+
prompt,
|
|
320
|
+
sessionId,
|
|
321
|
+
metadata: metadata ?? null,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// Run avec mutex (1 run par agent à la fois)
|
|
325
|
+
const result = await this.registry.withLock(validatedAgentName, async () => {
|
|
326
|
+
this.registry.markBusy(validatedAgentName, sessionId);
|
|
327
|
+
if (messageId && this.messageLog) {
|
|
328
|
+
await this.messageLog.markRunning(messageId, sessionId);
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const agentResult = await this.service.runAgent({
|
|
332
|
+
runner: runner,
|
|
333
|
+
prompt,
|
|
334
|
+
agentName: validatedAgentName,
|
|
335
|
+
sessionId,
|
|
336
|
+
path,
|
|
337
|
+
model,
|
|
338
|
+
mode,
|
|
339
|
+
silent,
|
|
340
|
+
});
|
|
341
|
+
const responseText = agentResult.content.map((c) => c.text).join('\n');
|
|
342
|
+
if (messageId && this.messageLog) {
|
|
343
|
+
await this.messageLog.markDone(messageId, responseText, agentResult.sessionId);
|
|
344
|
+
}
|
|
345
|
+
this.registry.markIdle(validatedAgentName, !agentResult.isError);
|
|
346
|
+
if (!agentResult.isError)
|
|
347
|
+
this.registry.markOnline(validatedAgentName);
|
|
348
|
+
return {
|
|
349
|
+
messageId,
|
|
350
|
+
sessionId: agentResult.sessionId,
|
|
351
|
+
content: agentResult.content,
|
|
352
|
+
isError: agentResult.isError,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
const errorMsg = err.message;
|
|
357
|
+
if (messageId && this.messageLog) {
|
|
358
|
+
if (errorMsg.includes('TIMEOUT') || errorMsg.includes('timeout')) {
|
|
359
|
+
await this.messageLog.markTimeout(messageId);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
await this.messageLog.markFailed(messageId, errorMsg);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
this.registry.markIdle(validatedAgentName, false);
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* agent.a2a — Agent A parle à Agent B (le hub orchestre).
|
|
373
|
+
* B reçoit un prompt enrichi avec le contexte de A.
|
|
374
|
+
*/
|
|
375
|
+
async methodAgentA2A(req) {
|
|
376
|
+
const params = this.validateParams(A2AParams, req.params, req.id);
|
|
377
|
+
if (!params.ok)
|
|
378
|
+
return params.response;
|
|
379
|
+
const { fromAgent, toAgent, runner, prompt, model, path, metadata } = params.data;
|
|
380
|
+
const validatedFrom = validateAgentName(fromAgent);
|
|
381
|
+
const validatedTo = validateAgentName(toAgent);
|
|
382
|
+
// Register les deux si pas vus
|
|
383
|
+
this.registry.register(validatedFrom, runner);
|
|
384
|
+
this.registry.register(validatedTo, runner);
|
|
385
|
+
this.registry.incrementA2aSent(validatedFrom);
|
|
386
|
+
this.registry.incrementA2aReceived(validatedTo);
|
|
387
|
+
// Enrichit le prompt avec contexte A→B
|
|
388
|
+
const enrichedPrompt = [
|
|
389
|
+
`[A2A — Agent-to-Agent Message]`,
|
|
390
|
+
`FROM: ${validatedFrom}`,
|
|
391
|
+
`TO: ${validatedTo}`,
|
|
392
|
+
`TIMESTAMP: ${new Date().toISOString()}`,
|
|
393
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
394
|
+
prompt,
|
|
395
|
+
].join('\n');
|
|
396
|
+
// Persist + run (délègue à agent.run)
|
|
397
|
+
let messageId;
|
|
398
|
+
if (this.messageLog) {
|
|
399
|
+
messageId = await this.messageLog.create({
|
|
400
|
+
fromAgent: validatedFrom,
|
|
401
|
+
toAgent: validatedTo,
|
|
402
|
+
runner: runner,
|
|
403
|
+
prompt: enrichedPrompt,
|
|
404
|
+
metadata: { ...(metadata ?? {}), a2a: true, from: validatedFrom, to: validatedTo },
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const result = await this.registry.withLock(validatedTo, async () => {
|
|
408
|
+
this.registry.markBusy(validatedTo);
|
|
409
|
+
if (messageId && this.messageLog) {
|
|
410
|
+
await this.messageLog.markRunning(messageId);
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const agentResult = await this.service.runAgent({
|
|
414
|
+
runner: runner,
|
|
415
|
+
prompt: enrichedPrompt,
|
|
416
|
+
agentName: validatedTo,
|
|
417
|
+
path,
|
|
418
|
+
model,
|
|
419
|
+
});
|
|
420
|
+
const responseText = agentResult.content.map((c) => c.text).join('\n');
|
|
421
|
+
if (messageId && this.messageLog) {
|
|
422
|
+
await this.messageLog.markDone(messageId, responseText, agentResult.sessionId);
|
|
423
|
+
}
|
|
424
|
+
this.registry.markIdle(validatedTo, !agentResult.isError);
|
|
425
|
+
if (!agentResult.isError)
|
|
426
|
+
this.registry.markOnline(validatedTo);
|
|
427
|
+
return {
|
|
428
|
+
messageId,
|
|
429
|
+
from: validatedFrom,
|
|
430
|
+
to: validatedTo,
|
|
431
|
+
sessionId: agentResult.sessionId,
|
|
432
|
+
content: agentResult.content,
|
|
433
|
+
isError: agentResult.isError,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
const errorMsg = err.message;
|
|
438
|
+
if (messageId && this.messageLog) {
|
|
439
|
+
if (errorMsg.includes('TIMEOUT') || errorMsg.includes('timeout')) {
|
|
440
|
+
await this.messageLog.markTimeout(messageId);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
await this.messageLog.markFailed(messageId, errorMsg);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
this.registry.markIdle(validatedTo, false);
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result };
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* agent.status — Status live d'un agent (busy/idle/online) via registry.
|
|
454
|
+
* Possibilité de proxy vers Overmind MCP agent_control aussi.
|
|
455
|
+
*/
|
|
456
|
+
async methodAgentStatus(req) {
|
|
457
|
+
const params = this.validateParams(AgentStatusParams, req.params, req.id);
|
|
458
|
+
if (!params.ok)
|
|
459
|
+
return params.response;
|
|
460
|
+
const { agentName, runner, action, sinceTimestamp, timeoutMs } = params.data;
|
|
461
|
+
const validatedName = validateAgentName(agentName);
|
|
462
|
+
// Status local du registry (instantané)
|
|
463
|
+
const localState = this.registry.get(validatedName);
|
|
464
|
+
// Si action demandée (status/stream/kill/wait), on proxy vers MCP
|
|
465
|
+
if (action) {
|
|
466
|
+
const result = await this.service.agentStatus({
|
|
467
|
+
agentName: validatedName,
|
|
468
|
+
action,
|
|
469
|
+
runner: runner,
|
|
470
|
+
sinceTimestamp,
|
|
471
|
+
timeoutMs,
|
|
472
|
+
});
|
|
473
|
+
// Sync registry avec retour MCP
|
|
474
|
+
if (action === 'status') {
|
|
475
|
+
const mcpState = this.parseAgentControlStatus(result);
|
|
476
|
+
if (mcpState === 'running') {
|
|
477
|
+
this.registry.markBusy(validatedName, result.sessionId);
|
|
478
|
+
}
|
|
479
|
+
else if (mcpState === 'done') {
|
|
480
|
+
this.registry.markIdle(validatedName, true);
|
|
481
|
+
this.registry.markOnline(validatedName);
|
|
482
|
+
}
|
|
483
|
+
else if (mcpState === 'failed') {
|
|
484
|
+
this.registry.markIdle(validatedName, false);
|
|
485
|
+
}
|
|
486
|
+
else if (mcpState === 'orphaned') {
|
|
487
|
+
this.registry.markOffline(validatedName);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (action === 'kill') {
|
|
491
|
+
this.registry.markOffline(validatedName);
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
jsonrpc: '2.0',
|
|
495
|
+
id: req.id ?? null,
|
|
496
|
+
result: { local: localState, mcp: result },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { local: localState } };
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* agent.list — Liste tous les agents et leur état.
|
|
503
|
+
*/
|
|
504
|
+
async methodAgentList(req) {
|
|
505
|
+
const params = this.validateParams(ListAgentsParams, req.params, req.id);
|
|
506
|
+
if (!params.ok)
|
|
507
|
+
return params.response;
|
|
508
|
+
const agents = this.registry.list({
|
|
509
|
+
status: params.data.status,
|
|
510
|
+
runner: params.data.runner,
|
|
511
|
+
});
|
|
512
|
+
const stats = this.registry.stats();
|
|
513
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { agents, stats } };
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* agent.kill — Kill un agent en cours.
|
|
517
|
+
*/
|
|
518
|
+
async methodAgentKill(req) {
|
|
519
|
+
const params = this.validateParams(z.object({ agentName: z.string().min(1), runner: z.string().optional() }), req.params, req.id);
|
|
520
|
+
if (!params.ok)
|
|
521
|
+
return params.response;
|
|
522
|
+
const result = await this.service.killAgent(validateAgentName(params.data.agentName), params.data.runner);
|
|
523
|
+
this.registry.markOffline(params.data.agentName);
|
|
524
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result };
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* message.history — Historique des messages persistés.
|
|
528
|
+
*/
|
|
529
|
+
async methodMessageHistory(req) {
|
|
530
|
+
if (!this.messageLog) {
|
|
531
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
532
|
+
}
|
|
533
|
+
const params = this.validateParams(MessageHistoryParams, req.params, req.id);
|
|
534
|
+
if (!params.ok)
|
|
535
|
+
return params.response;
|
|
536
|
+
const messages = await this.messageLog.list(params.data);
|
|
537
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { messages, count: messages.length } };
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* message.get — Récupère un message par ID.
|
|
541
|
+
*/
|
|
542
|
+
async methodMessageGet(req) {
|
|
543
|
+
if (!this.messageLog) {
|
|
544
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
545
|
+
}
|
|
546
|
+
const params = this.validateParams(MessageGetParams, req.params, req.id);
|
|
547
|
+
if (!params.ok)
|
|
548
|
+
return params.response;
|
|
549
|
+
const message = await this.messageLog.getById(params.data.id);
|
|
550
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { message } };
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* message.replay — Rejoue un message (re-run l'agent avec le même prompt).
|
|
554
|
+
* Le nouveau run crée un NOUVEAU message, l'ancien reste en status 'pending' pour traçabilité.
|
|
555
|
+
*/
|
|
556
|
+
async methodMessageReplay(req) {
|
|
557
|
+
if (!this.messageLog) {
|
|
558
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
559
|
+
}
|
|
560
|
+
const params = this.validateParams(MessageReplayParams, req.params, req.id);
|
|
561
|
+
if (!params.ok)
|
|
562
|
+
return params.response;
|
|
563
|
+
const original = await this.messageLog.getById(params.data.id);
|
|
564
|
+
if (!original) {
|
|
565
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32004, message: 'Message not found' } };
|
|
566
|
+
}
|
|
567
|
+
// Relance via agent.run
|
|
568
|
+
const replayReq = {
|
|
569
|
+
jsonrpc: '2.0',
|
|
570
|
+
id: req.id,
|
|
571
|
+
method: 'agent.run',
|
|
572
|
+
params: {
|
|
573
|
+
agentName: original.toAgent,
|
|
574
|
+
runner: original.runner,
|
|
575
|
+
prompt: original.prompt,
|
|
576
|
+
sessionId: original.sessionId ?? undefined,
|
|
577
|
+
metadata: { ...(original.metadata ?? {}), replayOf: original.id },
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
return this.methodAgentRun(replayReq);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* message.stats — Statistiques globales du log.
|
|
584
|
+
*/
|
|
585
|
+
async methodMessageStats(req) {
|
|
586
|
+
if (!this.messageLog) {
|
|
587
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
588
|
+
}
|
|
589
|
+
const stats = await this.messageLog.stats();
|
|
590
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: stats };
|
|
591
|
+
}
|
|
592
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
593
|
+
/**
|
|
594
|
+
* Valide les params d'une requête JSON-RPC via Zod.
|
|
595
|
+
* Retourne soit { data } (succès) soit une JsonRpcResponse d'erreur (à retourner tel quel).
|
|
596
|
+
*/
|
|
597
|
+
validateParams(schema, params, id) {
|
|
598
|
+
const result = schema.safeParse(params ?? {});
|
|
599
|
+
if (!result.success) {
|
|
600
|
+
return {
|
|
601
|
+
ok: false,
|
|
602
|
+
response: {
|
|
603
|
+
jsonrpc: '2.0',
|
|
604
|
+
id: id ?? null,
|
|
605
|
+
error: {
|
|
606
|
+
...JSON_RPC_ERRORS.INVALID_PARAMS,
|
|
607
|
+
data: result.error.issues,
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return { ok: true, data: result.data };
|
|
613
|
+
}
|
|
614
|
+
respondError(res, id, error) {
|
|
615
|
+
this.writeJson(res, 200, { jsonrpc: '2.0', id, error });
|
|
616
|
+
}
|
|
617
|
+
buildErrorResponse(id, err) {
|
|
618
|
+
const error = err;
|
|
619
|
+
return {
|
|
620
|
+
jsonrpc: '2.0',
|
|
621
|
+
id,
|
|
622
|
+
error: {
|
|
623
|
+
code: error.code ?? JSON_RPC_ERRORS.INTERNAL_ERROR.code,
|
|
624
|
+
message: error.message ?? 'Internal error',
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
readBody(req) {
|
|
629
|
+
return new Promise((resolve, reject) => {
|
|
630
|
+
const chunks = [];
|
|
631
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
632
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
633
|
+
req.on('error', reject);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Parse le retour de agent_control pour extraire le status de l'agent distant.
|
|
638
|
+
*/
|
|
639
|
+
parseAgentControlStatus(result) {
|
|
640
|
+
const text = result.content.map((c) => c.text).join('\n').toLowerCase();
|
|
641
|
+
if (text.includes('running') || text.includes('status: running'))
|
|
642
|
+
return 'running';
|
|
643
|
+
if (text.includes('done') || text.includes('completed'))
|
|
644
|
+
return 'done';
|
|
645
|
+
if (text.includes('failed') || text.includes('error'))
|
|
646
|
+
return 'failed';
|
|
647
|
+
if (text.includes('orphaned') || text.includes('offline'))
|
|
648
|
+
return 'orphaned';
|
|
649
|
+
return 'unknown';
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// ─── Safe Stats wrapper inline (déclaré ici pour éviter d'augmenter MessageLog) ───
|
|
653
|
+
// (Voir handleHealth — try/catch sur this.messageLog.stats())
|
|
654
|
+
//# sourceMappingURL=OverBridgeServer.js.map
|