overmind-mcp 2.8.13 → 2.8.14
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/bin/launch.js +78 -0
- package/dist/bin/overmind-bridge.d.ts +42 -0
- package/dist/bin/overmind-bridge.d.ts.map +1 -0
- package/dist/bin/overmind-bridge.js +503 -0
- package/dist/bin/overmind-bridge.js.map +1 -0
- package/dist/bridge/ArgParser.d.ts +45 -0
- package/dist/bridge/ArgParser.d.ts.map +1 -0
- package/dist/bridge/ArgParser.js +134 -0
- package/dist/bridge/ArgParser.js.map +1 -0
- package/dist/bridge/BridgeHttpClient.d.ts +61 -0
- package/dist/bridge/BridgeHttpClient.d.ts.map +1 -0
- package/dist/bridge/BridgeHttpClient.js +164 -0
- package/dist/bridge/BridgeHttpClient.js.map +1 -0
- package/dist/bridge/DirectiveParser.d.ts +82 -0
- package/dist/bridge/DirectiveParser.d.ts.map +1 -0
- package/dist/bridge/DirectiveParser.js +154 -0
- package/dist/bridge/DirectiveParser.js.map +1 -0
- package/dist/bridge/JsonSanitizer.d.ts +34 -0
- package/dist/bridge/JsonSanitizer.d.ts.map +1 -0
- package/dist/bridge/JsonSanitizer.js +90 -0
- package/dist/bridge/JsonSanitizer.js.map +1 -0
- package/dist/bridge/OverBridgeServer.d.ts +36 -2
- package/dist/bridge/OverBridgeServer.d.ts.map +1 -1
- package/dist/bridge/OverBridgeServer.js +351 -23
- package/dist/bridge/OverBridgeServer.js.map +1 -1
- package/dist/bridge/PromptSource.d.ts +66 -0
- package/dist/bridge/PromptSource.d.ts.map +1 -0
- package/dist/bridge/PromptSource.js +152 -0
- package/dist/bridge/PromptSource.js.map +1 -0
- package/dist/bridge/RequestContext.d.ts +19 -0
- package/dist/bridge/RequestContext.d.ts.map +1 -0
- package/dist/bridge/RequestContext.js +34 -0
- package/dist/bridge/RequestContext.js.map +1 -0
- package/dist/bridge/ScenarioLoader.d.ts +124 -0
- package/dist/bridge/ScenarioLoader.d.ts.map +1 -0
- package/dist/bridge/ScenarioLoader.js +333 -0
- package/dist/bridge/ScenarioLoader.js.map +1 -0
- package/dist/bridge/SessionStore.d.ts +109 -0
- package/dist/bridge/SessionStore.d.ts.map +1 -0
- package/dist/bridge/SessionStore.js +220 -0
- package/dist/bridge/SessionStore.js.map +1 -0
- package/dist/bridge/WebhookAdapter.d.ts +76 -0
- package/dist/bridge/WebhookAdapter.d.ts.map +1 -0
- package/dist/bridge/WebhookAdapter.js +186 -0
- package/dist/bridge/WebhookAdapter.js.map +1 -0
- package/dist/bridge/index.d.ts +16 -0
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +10 -0
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/utils.d.ts +9 -0
- package/dist/bridge/utils.d.ts.map +1 -1
- package/dist/bridge/utils.js +17 -0
- package/dist/bridge/utils.js.map +1 -1
- package/dist/services/NousHermesRunner.d.ts.map +1 -1
- package/dist/services/NousHermesRunner.js +47 -8
- package/dist/services/NousHermesRunner.js.map +1 -1
- package/package.json +2 -1
|
@@ -20,9 +20,16 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import http from 'node:http';
|
|
22
22
|
import { URL } from 'node:url';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import fs from 'node:fs';
|
|
23
25
|
import { z } from 'zod';
|
|
24
26
|
import { AgentRegistry } from './AgentRegistry.js';
|
|
25
27
|
import { MessageLog } from './MessageLog.js';
|
|
28
|
+
import { SessionStore } from './SessionStore.js';
|
|
29
|
+
import { DirectiveParser } from './DirectiveParser.js';
|
|
30
|
+
import { WebhookAdapter } from './WebhookAdapter.js';
|
|
31
|
+
import { sanitizeAndParse, looksLikeWindowsPathIssue } from './JsonSanitizer.js';
|
|
32
|
+
import { getOrCreateRequestId } from './RequestContext.js';
|
|
26
33
|
import { createBridgeLogger, validateAgentName } from './utils.js';
|
|
27
34
|
const JSON_RPC_ERRORS = {
|
|
28
35
|
PARSE_ERROR: { code: -32700, message: 'Parse error' },
|
|
@@ -44,6 +51,10 @@ const RunAgentParams = z.object({
|
|
|
44
51
|
mode: z.string().optional(),
|
|
45
52
|
silent: z.boolean().optional(),
|
|
46
53
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
54
|
+
/** Clé externe pour SessionStore (phone, userId, etc.) — auto-résolution de sessionId */
|
|
55
|
+
externalKey: z.string().optional(),
|
|
56
|
+
/** Active le parsing de directives dans la réponse (default: true si enableDirectives) */
|
|
57
|
+
parseDirectives: z.boolean().optional(),
|
|
47
58
|
});
|
|
48
59
|
const A2AParams = z.object({
|
|
49
60
|
fromAgent: z.string().min(1),
|
|
@@ -85,6 +96,9 @@ export class OverBridgeServer {
|
|
|
85
96
|
registry;
|
|
86
97
|
log;
|
|
87
98
|
messageLog;
|
|
99
|
+
sessions;
|
|
100
|
+
directiveParser;
|
|
101
|
+
webhookAdapter;
|
|
88
102
|
config;
|
|
89
103
|
server;
|
|
90
104
|
startTime = 0;
|
|
@@ -93,13 +107,22 @@ export class OverBridgeServer {
|
|
|
93
107
|
this.config = config;
|
|
94
108
|
this.log = logger ?? createBridgeLogger('overbridge-server');
|
|
95
109
|
this.registry = new AgentRegistry(this.log);
|
|
110
|
+
this.directiveParser = new DirectiveParser({ logger: this.log });
|
|
111
|
+
this.webhookAdapter = new WebhookAdapter({ logger: this.log });
|
|
96
112
|
if (config.enableMessageLog) {
|
|
97
113
|
this.messageLog = new MessageLog(config.postgres, this.log);
|
|
98
114
|
}
|
|
115
|
+
if (config.enableSessionStore) {
|
|
116
|
+
const defaultPath = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '.', '.overmind', 'bridge', 'sessions.json');
|
|
117
|
+
this.sessions = new SessionStore({
|
|
118
|
+
persistPath: config.sessionStorePath ?? defaultPath,
|
|
119
|
+
ttlMs: config.sessionTtlMs,
|
|
120
|
+
}, this.log);
|
|
121
|
+
}
|
|
99
122
|
}
|
|
100
123
|
// ─── Lifecycle ───────────────────────────────────────────────────────────
|
|
101
124
|
/**
|
|
102
|
-
* Démarre le serveur HTTP et initialise les dépendances (MessageLog, OverBridgeService).
|
|
125
|
+
* Démarre le serveur HTTP et initialise les dépendances (MessageLog, SessionStore, OverBridgeService).
|
|
103
126
|
*/
|
|
104
127
|
async start() {
|
|
105
128
|
this.startTime = Date.now();
|
|
@@ -107,7 +130,11 @@ export class OverBridgeServer {
|
|
|
107
130
|
if (this.messageLog) {
|
|
108
131
|
await this.messageLog.init();
|
|
109
132
|
}
|
|
110
|
-
// 2)
|
|
133
|
+
// 2) Init SessionStore
|
|
134
|
+
if (this.sessions) {
|
|
135
|
+
await this.sessions.init();
|
|
136
|
+
}
|
|
137
|
+
// 3) Connect OverBridgeService (MCP healthcheck)
|
|
111
138
|
try {
|
|
112
139
|
const status = await this.service.connect(this.config.healthCheckIntervalMs);
|
|
113
140
|
this.log.info(`🔌 OverBridgeService connected: ${status.status}`);
|
|
@@ -116,7 +143,7 @@ export class OverBridgeServer {
|
|
|
116
143
|
this.log.warn(`⚠️ OverBridgeService connect failed: ${err.message}`);
|
|
117
144
|
// On continue quand même, le serveur répondra avec erreurs si MCP down
|
|
118
145
|
}
|
|
119
|
-
//
|
|
146
|
+
// 4) Démarre le serveur HTTP
|
|
120
147
|
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
121
148
|
await new Promise((resolve) => {
|
|
122
149
|
this.server.listen(this.config.port, this.config.host, () => resolve());
|
|
@@ -127,6 +154,15 @@ export class OverBridgeServer {
|
|
|
127
154
|
this.log.info(`🚀 OverBridgeServer listening on ${url}`);
|
|
128
155
|
this.log.info(` POST ${url}/rpc (JSON-RPC 2.0)`);
|
|
129
156
|
this.log.info(` GET ${url}/health`);
|
|
157
|
+
if (this.config.enableWebhooks) {
|
|
158
|
+
this.log.info(` POST ${url}/webhook/:provider (voipms, twilio, discord, generic)`);
|
|
159
|
+
}
|
|
160
|
+
if (this.sessions) {
|
|
161
|
+
this.log.info(` SessionStore: enabled (TTL ${(this.config.sessionTtlMs ?? 14_400_000) / 1000}s)`);
|
|
162
|
+
}
|
|
163
|
+
if (this.config.enableDirectives) {
|
|
164
|
+
this.log.info(` DirectiveParser: enabled (SESSION_ID, CONTEXT_UPDATE, BRIDGE_NEXT)`);
|
|
165
|
+
}
|
|
130
166
|
return { port, host: this.config.host, url };
|
|
131
167
|
}
|
|
132
168
|
/**
|
|
@@ -142,6 +178,9 @@ export class OverBridgeServer {
|
|
|
142
178
|
if (this.messageLog) {
|
|
143
179
|
await this.messageLog.close();
|
|
144
180
|
}
|
|
181
|
+
if (this.sessions) {
|
|
182
|
+
await this.sessions.close();
|
|
183
|
+
}
|
|
145
184
|
}
|
|
146
185
|
/**
|
|
147
186
|
* Expose le registry (pour tests / inspection).
|
|
@@ -155,9 +194,18 @@ export class OverBridgeServer {
|
|
|
155
194
|
get messages() {
|
|
156
195
|
return this.messageLog;
|
|
157
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Expose le SessionStore (pour tests / inspection).
|
|
199
|
+
*/
|
|
200
|
+
get sessionStore() {
|
|
201
|
+
return this.sessions;
|
|
202
|
+
}
|
|
158
203
|
// ─── HTTP Request Handler ───────────────────────────────────────────────
|
|
159
204
|
async handleRequest(req, res) {
|
|
160
205
|
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
206
|
+
const reqId = getOrCreateRequestId(req.headers);
|
|
207
|
+
// Set reqId header in response
|
|
208
|
+
res.setHeader('X-Request-Id', reqId);
|
|
161
209
|
// CORS preflight
|
|
162
210
|
if (req.method === 'OPTIONS') {
|
|
163
211
|
this.writeCors(res);
|
|
@@ -174,15 +222,25 @@ export class OverBridgeServer {
|
|
|
174
222
|
}
|
|
175
223
|
// JSON-RPC endpoint
|
|
176
224
|
if (req.method === 'POST' && url.pathname === '/rpc') {
|
|
177
|
-
await this.handleRpc(req, res);
|
|
225
|
+
await this.handleRpc(req, res, reqId);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Webhook endpoint (si activé) — /webhook/:provider
|
|
229
|
+
if (this.config.enableWebhooks && req.method === 'POST' && url.pathname.startsWith('/webhook/')) {
|
|
230
|
+
await this.handleWebhook(req, res, url, reqId);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// File serve — /f/:filename (statique, comme bt-sms)
|
|
234
|
+
if (req.method === 'GET' && url.pathname.startsWith('/f/')) {
|
|
235
|
+
await this.handleStaticFile(req, res, url);
|
|
178
236
|
return;
|
|
179
237
|
}
|
|
180
238
|
// 404
|
|
181
|
-
this.writeJson(res, 404, { error: 'Not found', path: url.pathname });
|
|
239
|
+
this.writeJson(res, 404, { error: 'Not found', path: url.pathname, reqId });
|
|
182
240
|
}
|
|
183
241
|
catch (err) {
|
|
184
|
-
this.log.error(`💥 Unhandled error: ${err.message}`);
|
|
185
|
-
this.writeJson(res, 500, { error: 'Internal server error' });
|
|
242
|
+
this.log.error(`💥 Unhandled error: ${err.message} (reqId=${reqId})`);
|
|
243
|
+
this.writeJson(res, 500, { error: 'Internal server error', reqId });
|
|
186
244
|
}
|
|
187
245
|
}
|
|
188
246
|
writeCors(res) {
|
|
@@ -194,6 +252,77 @@ export class OverBridgeServer {
|
|
|
194
252
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
195
253
|
res.end(JSON.stringify(body));
|
|
196
254
|
}
|
|
255
|
+
// ─── /webhook/:provider Endpoint ─────────────────────────────────────────
|
|
256
|
+
async handleWebhook(req, res, url, reqId) {
|
|
257
|
+
// /webhook/:provider — extrait le provider du path
|
|
258
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
259
|
+
const provider = (parts[1] ?? 'voipms');
|
|
260
|
+
const raw = await this.readBody(req);
|
|
261
|
+
let payload;
|
|
262
|
+
try {
|
|
263
|
+
payload = JSON.parse(raw);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Tente sanitizer (VoIP.ms envoie parfois du form-encoded mal formé)
|
|
267
|
+
try {
|
|
268
|
+
payload = sanitizeAndParse(raw);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
this.writeJson(res, 400, { error: 'Invalid JSON body', reqId });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Adapte via WebhookAdapter
|
|
276
|
+
const adapter = new WebhookAdapter({ provider, logger: this.log });
|
|
277
|
+
let normalized;
|
|
278
|
+
try {
|
|
279
|
+
normalized = adapter.adapt(payload);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
this.writeJson(res, 400, { error: err.message, reqId });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Auto-dispatch vers agent.run via internal call
|
|
286
|
+
this.log.info(`[reqId=${reqId}] 📨 Webhook ${provider} from ${normalized.externalKey}`);
|
|
287
|
+
// Cherche un agent par défaut (config-driven ou fallback premier agent)
|
|
288
|
+
// Pour l'instant on retourne le normalized et on log ; les utilisateurs
|
|
289
|
+
// peuvent soit :
|
|
290
|
+
// 1. Définir une config par-provider pour auto-dispatch
|
|
291
|
+
// 2. Appeler manuellement /rpc agent.run après réception
|
|
292
|
+
this.writeJson(res, 200, {
|
|
293
|
+
received: true,
|
|
294
|
+
reqId,
|
|
295
|
+
provider,
|
|
296
|
+
externalKey: normalized.externalKey,
|
|
297
|
+
promptPreview: normalized.prompt.slice(0, 200),
|
|
298
|
+
mediaCount: normalized.mediaUrls.length,
|
|
299
|
+
metadata: normalized.metadata,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
// ─── /f/:filename Static File Serve ──────────────────────────────────────
|
|
303
|
+
async handleStaticFile(_req, res, url) {
|
|
304
|
+
const filename = url.pathname.slice(3); // strip /f/
|
|
305
|
+
if (!filename || filename.includes('..')) {
|
|
306
|
+
this.writeJson(res, 400, { error: 'Invalid filename' });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Le dossier est configuré via BRIDGE_STATIC_DIR env ou défaut './public'
|
|
310
|
+
const staticDir = process.env.BRIDGE_STATIC_DIR ?? './public';
|
|
311
|
+
const filepath = path.join(staticDir, filename);
|
|
312
|
+
if (!fs.existsSync(filepath)) {
|
|
313
|
+
this.writeJson(res, 404, { error: 'Not found' });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
317
|
+
const mimeMap = {
|
|
318
|
+
'.html': 'text/html', '.pdf': 'application/pdf', '.png': 'image/png',
|
|
319
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.json': 'application/json',
|
|
320
|
+
'.txt': 'text/plain', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4',
|
|
321
|
+
'.gif': 'image/gif', '.wav': 'audio/wav', '.xml': 'text/xml',
|
|
322
|
+
};
|
|
323
|
+
res.setHeader('Content-Type', mimeMap[ext] ?? 'application/octet-stream');
|
|
324
|
+
fs.createReadStream(filepath).pipe(res);
|
|
325
|
+
}
|
|
197
326
|
// ─── /health Endpoint ───────────────────────────────────────────────────
|
|
198
327
|
async handleHealth(res) {
|
|
199
328
|
const mcpHealth = await this.service.proxyAccess.healthCheck();
|
|
@@ -207,17 +336,26 @@ export class OverBridgeServer {
|
|
|
207
336
|
msgStats = null;
|
|
208
337
|
}
|
|
209
338
|
}
|
|
339
|
+
const sessionStats = this.sessions ? this.sessions.stats() : null;
|
|
210
340
|
this.writeJson(res, 200, {
|
|
211
341
|
status: mcpHealth.status,
|
|
212
342
|
uptime: Date.now() - this.startTime,
|
|
213
343
|
mcp: mcpHealth,
|
|
214
344
|
agents: regStats,
|
|
215
345
|
messages: msgStats,
|
|
216
|
-
|
|
346
|
+
sessions: sessionStats,
|
|
347
|
+
features: {
|
|
348
|
+
sessionStore: !!this.sessions,
|
|
349
|
+
directives: !!this.config.enableDirectives,
|
|
350
|
+
webhooks: !!this.config.enableWebhooks,
|
|
351
|
+
sanitizeJson: !!this.config.sanitizeJson,
|
|
352
|
+
messageLog: !!this.messageLog,
|
|
353
|
+
},
|
|
354
|
+
version: '1.1.0',
|
|
217
355
|
});
|
|
218
356
|
}
|
|
219
357
|
// ─── /rpc Endpoint (JSON-RPC 2.0 Dispatcher) ────────────────────────────
|
|
220
|
-
async handleRpc(req, res) {
|
|
358
|
+
async handleRpc(req, res, reqId) {
|
|
221
359
|
// Auth check
|
|
222
360
|
if (this.config.authToken) {
|
|
223
361
|
const auth = req.headers.authorization;
|
|
@@ -225,7 +363,7 @@ export class OverBridgeServer {
|
|
|
225
363
|
this.writeJson(res, 401, {
|
|
226
364
|
jsonrpc: '2.0',
|
|
227
365
|
id: null,
|
|
228
|
-
error: { code: -32000, message: 'Unauthorized' },
|
|
366
|
+
error: { code: -32000, message: 'Unauthorized', data: { reqId } },
|
|
229
367
|
});
|
|
230
368
|
return;
|
|
231
369
|
}
|
|
@@ -234,12 +372,39 @@ export class OverBridgeServer {
|
|
|
234
372
|
const raw = await this.readBody(req);
|
|
235
373
|
let parsed;
|
|
236
374
|
try {
|
|
375
|
+
// Try direct parse first
|
|
237
376
|
parsed = JSON.parse(raw);
|
|
238
377
|
}
|
|
239
378
|
catch {
|
|
240
|
-
|
|
241
|
-
|
|
379
|
+
// Si sanitizer activé et body ressemble à un Windows path, on tente la repair
|
|
380
|
+
if (this.config.sanitizeJson && looksLikeWindowsPathIssue(raw)) {
|
|
381
|
+
try {
|
|
382
|
+
parsed = sanitizeAndParse(raw);
|
|
383
|
+
this.log.warn(`[reqId=${reqId}] ⚠️ JSON body sanitized (Windows path issue)`);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
242
394
|
}
|
|
395
|
+
// Inject reqId into all calls (pour corrélation)
|
|
396
|
+
const injectReqId = (reqs) => {
|
|
397
|
+
const inject = (r) => {
|
|
398
|
+
if (!r.params)
|
|
399
|
+
r.params = {};
|
|
400
|
+
r.params.__reqId = reqId;
|
|
401
|
+
};
|
|
402
|
+
if (Array.isArray(reqs))
|
|
403
|
+
reqs.forEach(inject);
|
|
404
|
+
else
|
|
405
|
+
inject(reqs);
|
|
406
|
+
};
|
|
407
|
+
injectReqId(parsed);
|
|
243
408
|
// Batch ou single
|
|
244
409
|
if (Array.isArray(parsed)) {
|
|
245
410
|
if (parsed.length === 0) {
|
|
@@ -282,6 +447,16 @@ export class OverBridgeServer {
|
|
|
282
447
|
return this.methodMessageReplay(req);
|
|
283
448
|
case 'message.stats':
|
|
284
449
|
return this.methodMessageStats(req);
|
|
450
|
+
case 'session.get':
|
|
451
|
+
return this.methodSessionGet(req);
|
|
452
|
+
case 'session.list':
|
|
453
|
+
return this.methodSessionList(req);
|
|
454
|
+
case 'session.delete':
|
|
455
|
+
return this.methodSessionDelete(req);
|
|
456
|
+
case 'session.stats':
|
|
457
|
+
return this.methodSessionStats(req);
|
|
458
|
+
case 'webhook.sms':
|
|
459
|
+
return this.methodWebhookSms(req);
|
|
285
460
|
case 'health.ping':
|
|
286
461
|
return { jsonrpc: '2.0', id: req.id ?? null, result: { pong: true, ts: Date.now() } };
|
|
287
462
|
default:
|
|
@@ -300,15 +475,26 @@ export class OverBridgeServer {
|
|
|
300
475
|
// ─── RPC Methods ─────────────────────────────────────────────────────────
|
|
301
476
|
/**
|
|
302
477
|
* agent.run — Lance un agent (from client externe OU from autre agent).
|
|
478
|
+
* Avec support SessionStore (externalKey) et DirectiveParser.
|
|
303
479
|
*/
|
|
304
480
|
async methodAgentRun(req) {
|
|
305
481
|
const params = this.validateParams(RunAgentParams, req.params, req.id);
|
|
306
482
|
if (!params.ok)
|
|
307
483
|
return params.response;
|
|
308
|
-
const { agentName, runner, prompt, sessionId, path, model, mode, silent, metadata } = params.data;
|
|
484
|
+
const { agentName, runner, prompt, sessionId, path, model, mode, silent, metadata, externalKey, parseDirectives } = params.data;
|
|
309
485
|
const validatedAgentName = validateAgentName(agentName);
|
|
486
|
+
const reqId = req.params?.__reqId;
|
|
310
487
|
// Register
|
|
311
488
|
this.registry.register(validatedAgentName, runner);
|
|
489
|
+
// SessionStore resolution (si externalKey fourni)
|
|
490
|
+
let effectiveSessionId = sessionId;
|
|
491
|
+
if (this.sessions && externalKey) {
|
|
492
|
+
const stored = this.sessions.get(externalKey, validatedAgentName);
|
|
493
|
+
if (stored) {
|
|
494
|
+
effectiveSessionId = stored.sessionId;
|
|
495
|
+
this.log.info(`[reqId=${reqId}] 🔗 Session restored for ${externalKey} → ${effectiveSessionId}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
312
498
|
// Persist (pending)
|
|
313
499
|
let messageId;
|
|
314
500
|
if (this.messageLog) {
|
|
@@ -317,39 +503,79 @@ export class OverBridgeServer {
|
|
|
317
503
|
toAgent: validatedAgentName,
|
|
318
504
|
runner: runner,
|
|
319
505
|
prompt,
|
|
320
|
-
sessionId,
|
|
321
|
-
metadata: metadata ??
|
|
506
|
+
sessionId: effectiveSessionId,
|
|
507
|
+
metadata: { ...(metadata ?? {}), ...(reqId ? { reqId } : {}), ...(externalKey ? { externalKey } : {}) },
|
|
322
508
|
});
|
|
323
509
|
}
|
|
324
510
|
// Run avec mutex (1 run par agent à la fois)
|
|
325
511
|
const result = await this.registry.withLock(validatedAgentName, async () => {
|
|
326
|
-
this.registry.markBusy(validatedAgentName,
|
|
512
|
+
this.registry.markBusy(validatedAgentName, effectiveSessionId);
|
|
327
513
|
if (messageId && this.messageLog) {
|
|
328
|
-
await this.messageLog.markRunning(messageId,
|
|
514
|
+
await this.messageLog.markRunning(messageId, effectiveSessionId);
|
|
329
515
|
}
|
|
330
516
|
try {
|
|
331
517
|
const agentResult = await this.service.runAgent({
|
|
332
518
|
runner: runner,
|
|
333
519
|
prompt,
|
|
334
520
|
agentName: validatedAgentName,
|
|
335
|
-
sessionId,
|
|
521
|
+
sessionId: effectiveSessionId,
|
|
336
522
|
path,
|
|
337
523
|
model,
|
|
338
524
|
mode,
|
|
339
525
|
silent,
|
|
340
526
|
});
|
|
341
|
-
const
|
|
527
|
+
const rawResponseText = agentResult.content.map((c) => c.text).join('\n');
|
|
528
|
+
// ─── Directive parsing ─────────────────────────────────────────
|
|
529
|
+
let cleanText = rawResponseText;
|
|
530
|
+
let directives;
|
|
531
|
+
const shouldParseDirectives = parseDirectives ?? this.config.enableDirectives;
|
|
532
|
+
if (shouldParseDirectives) {
|
|
533
|
+
directives = this.directiveParser.parse(rawResponseText);
|
|
534
|
+
cleanText = directives.cleanText;
|
|
535
|
+
for (const action of directives.actions) {
|
|
536
|
+
if (action.kind === 'session' && this.sessions && externalKey) {
|
|
537
|
+
this.sessions.updateSessionId(externalKey, validatedAgentName, action.sessionId, runner);
|
|
538
|
+
this.log.info(`[reqId=${reqId}] 🔗 SESSION_ID directive → ${action.sessionId}`);
|
|
539
|
+
}
|
|
540
|
+
else if (action.kind === 'context' && this.sessions && externalKey) {
|
|
541
|
+
this.sessions.updateContext(externalKey, validatedAgentName, action.patch);
|
|
542
|
+
this.log.info(`[reqId=${reqId}] 📝 CONTEXT_UPDATE directive → ${JSON.stringify(action.patch)}`);
|
|
543
|
+
}
|
|
544
|
+
else if (action.kind === 'hint') {
|
|
545
|
+
this.log.info(`[reqId=${reqId}] 💡 BRIDGE_HINT: ${action.text}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Save session
|
|
550
|
+
if (this.sessions && externalKey && agentResult.sessionId) {
|
|
551
|
+
this.sessions.set({
|
|
552
|
+
externalKey,
|
|
553
|
+
agentName: validatedAgentName,
|
|
554
|
+
runner: runner,
|
|
555
|
+
sessionId: agentResult.sessionId,
|
|
556
|
+
context: directives?.actions.find((a) => a.kind === 'context')
|
|
557
|
+
? directives.actions.find((a) => a.kind === 'context').patch
|
|
558
|
+
: undefined,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
342
561
|
if (messageId && this.messageLog) {
|
|
343
|
-
await this.messageLog.markDone(messageId,
|
|
562
|
+
await this.messageLog.markDone(messageId, cleanText, agentResult.sessionId);
|
|
344
563
|
}
|
|
345
564
|
this.registry.markIdle(validatedAgentName, !agentResult.isError);
|
|
346
565
|
if (!agentResult.isError)
|
|
347
566
|
this.registry.markOnline(validatedAgentName);
|
|
567
|
+
// Return content nettoyé des directives (si parsing activé)
|
|
568
|
+
const resultContent = shouldParseDirectives
|
|
569
|
+
? [{ type: 'text', text: cleanText }]
|
|
570
|
+
: agentResult.content;
|
|
348
571
|
return {
|
|
349
572
|
messageId,
|
|
350
573
|
sessionId: agentResult.sessionId,
|
|
351
|
-
content:
|
|
574
|
+
content: resultContent,
|
|
352
575
|
isError: agentResult.isError,
|
|
576
|
+
directives: shouldParseDirectives
|
|
577
|
+
? directives?.actions.map((a) => a.kind)
|
|
578
|
+
: undefined,
|
|
353
579
|
};
|
|
354
580
|
}
|
|
355
581
|
catch (err) {
|
|
@@ -589,6 +815,84 @@ export class OverBridgeServer {
|
|
|
589
815
|
const stats = await this.messageLog.stats();
|
|
590
816
|
return { jsonrpc: '2.0', id: req.id ?? null, result: stats };
|
|
591
817
|
}
|
|
818
|
+
// ─── SessionStore RPC Methods ───────────────────────────────────────────
|
|
819
|
+
async methodSessionGet(req) {
|
|
820
|
+
if (!this.sessions) {
|
|
821
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
822
|
+
}
|
|
823
|
+
const params = this.validateParams(z.object({ externalKey: z.string().min(1), agentName: z.string().min(1) }), req.params, req.id);
|
|
824
|
+
if (!params.ok)
|
|
825
|
+
return params.response;
|
|
826
|
+
const entry = this.sessions.get(params.data.externalKey, params.data.agentName);
|
|
827
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { session: entry } };
|
|
828
|
+
}
|
|
829
|
+
async methodSessionList(_req) {
|
|
830
|
+
if (!this.sessions) {
|
|
831
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
832
|
+
}
|
|
833
|
+
const list = this.sessions.list();
|
|
834
|
+
const stats = this.sessions.stats();
|
|
835
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, result: { sessions: list, stats } };
|
|
836
|
+
}
|
|
837
|
+
async methodSessionDelete(req) {
|
|
838
|
+
if (!this.sessions) {
|
|
839
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
840
|
+
}
|
|
841
|
+
const params = this.validateParams(z.object({ externalKey: z.string().min(1), agentName: z.string().min(1) }), req.params, req.id);
|
|
842
|
+
if (!params.ok)
|
|
843
|
+
return params.response;
|
|
844
|
+
const deleted = this.sessions.delete(params.data.externalKey, params.data.agentName);
|
|
845
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { deleted } };
|
|
846
|
+
}
|
|
847
|
+
async methodSessionStats(_req) {
|
|
848
|
+
if (!this.sessions) {
|
|
849
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
850
|
+
}
|
|
851
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, result: this.sessions.stats() };
|
|
852
|
+
}
|
|
853
|
+
// ─── Webhook RPC Method (programmatic, sans passer par HTTP) ────────────
|
|
854
|
+
async methodWebhookSms(req) {
|
|
855
|
+
const params = this.validateParams(z.object({
|
|
856
|
+
provider: z.enum(['voipms', 'twilio', 'discord', 'generic']).default('voipms'),
|
|
857
|
+
payload: z.record(z.string(), z.unknown()),
|
|
858
|
+
externalKey: z.string().optional(),
|
|
859
|
+
/** Si fourni, dispatch automatique vers agent.run après adaptation */
|
|
860
|
+
autoDispatch: z.object({
|
|
861
|
+
agentName: z.string().min(1),
|
|
862
|
+
runner: z.string().min(1),
|
|
863
|
+
model: z.string().optional(),
|
|
864
|
+
mode: z.string().optional(),
|
|
865
|
+
}).optional(),
|
|
866
|
+
}), req.params, req.id);
|
|
867
|
+
if (!params.ok)
|
|
868
|
+
return params.response;
|
|
869
|
+
const adapter = new WebhookAdapter({ provider: params.data.provider, logger: this.log });
|
|
870
|
+
const normalized = adapter.adapt(params.data.payload);
|
|
871
|
+
const externalKey = params.data.externalKey ?? normalized.externalKey;
|
|
872
|
+
if (!params.data.autoDispatch) {
|
|
873
|
+
return {
|
|
874
|
+
jsonrpc: '2.0',
|
|
875
|
+
id: req.id ?? null,
|
|
876
|
+
result: { externalKey, prompt: normalized.prompt, mediaUrls: normalized.mediaUrls, metadata: normalized.metadata },
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
// Auto-dispatch vers agent.run
|
|
880
|
+
const innerReq = {
|
|
881
|
+
jsonrpc: '2.0',
|
|
882
|
+
id: req.id,
|
|
883
|
+
method: 'agent.run',
|
|
884
|
+
params: {
|
|
885
|
+
agentName: params.data.autoDispatch.agentName,
|
|
886
|
+
runner: params.data.autoDispatch.runner,
|
|
887
|
+
prompt: normalized.prompt,
|
|
888
|
+
model: params.data.autoDispatch.model,
|
|
889
|
+
mode: params.data.autoDispatch.mode,
|
|
890
|
+
externalKey,
|
|
891
|
+
metadata: { webhook: true, provider: params.data.provider, ...normalized.metadata },
|
|
892
|
+
},
|
|
893
|
+
};
|
|
894
|
+
return this.methodAgentRun(innerReq);
|
|
895
|
+
}
|
|
592
896
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
593
897
|
/**
|
|
594
898
|
* Valide les params d'une requête JSON-RPC via Zod.
|
|
@@ -625,14 +929,38 @@ export class OverBridgeServer {
|
|
|
625
929
|
},
|
|
626
930
|
};
|
|
627
931
|
}
|
|
628
|
-
readBody(req) {
|
|
932
|
+
readBody(req, maxBytes) {
|
|
933
|
+
const limit = maxBytes ?? this.parseBodyLimit(this.config.jsonBodyLimit);
|
|
629
934
|
return new Promise((resolve, reject) => {
|
|
630
935
|
const chunks = [];
|
|
631
|
-
|
|
936
|
+
let total = 0;
|
|
937
|
+
req.on('data', (chunk) => {
|
|
938
|
+
total += chunk.length;
|
|
939
|
+
if (total > limit) {
|
|
940
|
+
req.destroy();
|
|
941
|
+
reject(Object.assign(new Error('Request body too large'), { code: 'EBODYTOOLARGE' }));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
chunks.push(chunk);
|
|
945
|
+
});
|
|
632
946
|
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
633
947
|
req.on('error', reject);
|
|
634
948
|
});
|
|
635
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Parse "10mb" / "1gb" en bytes.
|
|
952
|
+
*/
|
|
953
|
+
parseBodyLimit(limit) {
|
|
954
|
+
if (!limit)
|
|
955
|
+
return 10 * 1024 * 1024; // 10mb default
|
|
956
|
+
const m = limit.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i);
|
|
957
|
+
if (!m)
|
|
958
|
+
return 10 * 1024 * 1024;
|
|
959
|
+
const num = Number(m[1]);
|
|
960
|
+
const unit = (m[2] ?? 'b').toLowerCase();
|
|
961
|
+
const mult = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
|
|
962
|
+
return Math.floor(num * (mult[unit] ?? 1));
|
|
963
|
+
}
|
|
636
964
|
/**
|
|
637
965
|
* Parse le retour de agent_control pour extraire le status de l'agent distant.
|
|
638
966
|
*/
|