@openagents-org/agent-connector 0.1.0 → 0.1.2
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/package.json +1 -1
- package/src/cli.js +7 -7
- package/src/daemon.js +284 -39
- package/src/workspace-client.js +108 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -63,17 +63,17 @@ function table(rows, headers) {
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
65
|
async function cmdUp(connector, flags) {
|
|
66
|
-
const pid = connector.getDaemonPid();
|
|
67
|
-
if (pid) {
|
|
68
|
-
print(`Daemon already running (PID ${pid})`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
66
|
if (flags.foreground) {
|
|
73
|
-
// Run in foreground (used by daemonize child)
|
|
67
|
+
// Run in foreground (used by daemonize child) — skip PID check
|
|
68
|
+
// because the parent already wrote our PID to the file.
|
|
74
69
|
const daemon = connector.createDaemon();
|
|
75
70
|
await daemon.start();
|
|
76
71
|
} else {
|
|
72
|
+
const pid = connector.getDaemonPid();
|
|
73
|
+
if (pid) {
|
|
74
|
+
print(`Daemon already running (PID ${pid})`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
77
|
// Daemonize
|
|
78
78
|
const foregroundArgs = [process.argv[1], 'up', '--foreground'];
|
|
79
79
|
if (flags.config) foregroundArgs.push('--config', flags.config);
|
package/src/daemon.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { spawn, execSync } = require('child_process');
|
|
5
|
+
const { spawn, execSync, execFileSync } = require('child_process');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const { WorkspaceClient } = require('./workspace-client');
|
|
7
8
|
|
|
8
9
|
const IS_WINDOWS = process.platform === 'win32';
|
|
9
10
|
|
|
@@ -145,41 +146,28 @@ class Daemon {
|
|
|
145
146
|
* The parent process prints info and exits; the child runs `start()`.
|
|
146
147
|
* @param {string[]} foregroundArgs - CLI args for the foreground child process
|
|
147
148
|
*/
|
|
148
|
-
static daemonize(configDir, foregroundArgs) {
|
|
149
|
+
static daemonize(configDir, foregroundArgs, execPath) {
|
|
149
150
|
const logFile = path.join(configDir, 'daemon.log');
|
|
150
151
|
const pidFile = path.join(configDir, 'daemon.pid');
|
|
152
|
+
const bin = execPath || process.execPath;
|
|
151
153
|
|
|
152
154
|
fs.mkdirSync(configDir, { recursive: true });
|
|
153
155
|
const logFd = fs.openSync(logFile, 'a');
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
} else {
|
|
170
|
-
// Unix: spawn detached child
|
|
171
|
-
const proc = spawn(process.execPath, foregroundArgs, {
|
|
172
|
-
detached: true,
|
|
173
|
-
stdio: ['ignore', logFd, logFd],
|
|
174
|
-
env: { ...process.env },
|
|
175
|
-
});
|
|
176
|
-
proc.unref();
|
|
177
|
-
fs.writeFileSync(pidFile, String(proc.pid), 'utf-8');
|
|
178
|
-
fs.closeSync(logFd);
|
|
179
|
-
console.log(`Daemon started (PID ${proc.pid})`);
|
|
180
|
-
console.log(`Logs: ${logFile}`);
|
|
181
|
-
console.log('Stop: agent-connector down');
|
|
182
|
-
}
|
|
157
|
+
const opts = {
|
|
158
|
+
detached: true,
|
|
159
|
+
stdio: ['ignore', logFd, logFd],
|
|
160
|
+
env: { ...process.env },
|
|
161
|
+
};
|
|
162
|
+
if (IS_WINDOWS) opts.windowsHide = true;
|
|
163
|
+
|
|
164
|
+
const proc = spawn(bin, foregroundArgs, opts);
|
|
165
|
+
proc.unref();
|
|
166
|
+
fs.writeFileSync(pidFile, String(proc.pid), 'utf-8');
|
|
167
|
+
fs.closeSync(logFd);
|
|
168
|
+
console.log(`Daemon started (PID ${proc.pid})`);
|
|
169
|
+
console.log(`Logs: ${logFile}`);
|
|
170
|
+
console.log('Stop: agent-connector down');
|
|
183
171
|
}
|
|
184
172
|
|
|
185
173
|
/**
|
|
@@ -195,7 +183,7 @@ class Daemon {
|
|
|
195
183
|
|
|
196
184
|
try {
|
|
197
185
|
if (IS_WINDOWS) {
|
|
198
|
-
execSync(`taskkill /PID ${pid}`, { stdio: 'ignore', timeout: 5000 });
|
|
186
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', timeout: 5000 });
|
|
199
187
|
} else {
|
|
200
188
|
process.kill(pid, 'SIGTERM');
|
|
201
189
|
}
|
|
@@ -209,8 +197,8 @@ class Daemon {
|
|
|
209
197
|
return true;
|
|
210
198
|
}
|
|
211
199
|
// Busy-wait 500ms (sync, used only in CLI stop command)
|
|
212
|
-
execSync(IS_WINDOWS ? '
|
|
213
|
-
stdio: 'ignore', timeout:
|
|
200
|
+
execSync(IS_WINDOWS ? 'ping -n 2 127.0.0.1 >nul' : 'sleep 0.5', {
|
|
201
|
+
stdio: 'ignore', timeout: 5000,
|
|
214
202
|
});
|
|
215
203
|
}
|
|
216
204
|
|
|
@@ -257,7 +245,19 @@ class Daemon {
|
|
|
257
245
|
};
|
|
258
246
|
this._processes[name] = info;
|
|
259
247
|
|
|
260
|
-
|
|
248
|
+
// Workspace-connected agents use the adapter loop (poll + CLI per message).
|
|
249
|
+
// Local-only agents use the spawn loop (long-running child process).
|
|
250
|
+
const network = agentCfg.network
|
|
251
|
+
? this.config.getNetworks().find(
|
|
252
|
+
(n) => n.slug === agentCfg.network || n.id === agentCfg.network
|
|
253
|
+
)
|
|
254
|
+
: null;
|
|
255
|
+
|
|
256
|
+
if (network) {
|
|
257
|
+
this._adapterLoop(name, agentCfg, info, network);
|
|
258
|
+
} else {
|
|
259
|
+
this._spawnLoop(name, agentCfg, info);
|
|
260
|
+
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
async _spawnLoop(name, agentCfg, info) {
|
|
@@ -277,6 +277,7 @@ class Daemon {
|
|
|
277
277
|
info.state = 'starting';
|
|
278
278
|
this._writeStatus();
|
|
279
279
|
|
|
280
|
+
this._log(`${name} launching: ${cmd.join(' ')}`);
|
|
280
281
|
const proc = this._spawnAgent(cmd, { env, cwd });
|
|
281
282
|
info.proc = proc;
|
|
282
283
|
info.state = 'running';
|
|
@@ -333,6 +334,234 @@ class Daemon {
|
|
|
333
334
|
this._writeStatus();
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Internal — adapter loop (workspace-connected agents)
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
async _adapterLoop(name, agentCfg, info, network) {
|
|
342
|
+
const wsClient = new WorkspaceClient(network.endpoint || 'https://workspace-endpoint.openagents.org');
|
|
343
|
+
const binary = this._resolveAgentBinary(agentCfg);
|
|
344
|
+
const env = this._buildAgentEnv(agentCfg);
|
|
345
|
+
let cursor = null;
|
|
346
|
+
const processedIds = new Set();
|
|
347
|
+
|
|
348
|
+
// Skip existing events on startup
|
|
349
|
+
try {
|
|
350
|
+
while (true) {
|
|
351
|
+
const { cursor: newCursor } = await wsClient.pollPending(
|
|
352
|
+
network.id, name, network.token, { after: cursor, limit: 200 }
|
|
353
|
+
);
|
|
354
|
+
if (!newCursor || newCursor === cursor) break;
|
|
355
|
+
cursor = newCursor;
|
|
356
|
+
}
|
|
357
|
+
this._log(`${name} skipped existing events, cursor=${cursor}`);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
this._log(`${name} skip-events failed: ${e.message}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Heartbeat interval
|
|
363
|
+
const heartbeatInterval = setInterval(async () => {
|
|
364
|
+
if (this._stoppedAgents.has(name) || this._shuttingDown) return;
|
|
365
|
+
try {
|
|
366
|
+
await wsClient.heartbeat(network.id, name, network.token);
|
|
367
|
+
} catch (e) {
|
|
368
|
+
this._log(`${name} heartbeat failed: ${e.message}`);
|
|
369
|
+
}
|
|
370
|
+
}, 30000);
|
|
371
|
+
|
|
372
|
+
// Send initial heartbeat
|
|
373
|
+
try { await wsClient.heartbeat(network.id, name, network.token); } catch {}
|
|
374
|
+
|
|
375
|
+
info.state = 'running';
|
|
376
|
+
info.startedAt = new Date().toISOString();
|
|
377
|
+
this._writeStatus();
|
|
378
|
+
this._log(`${name} adapter online → ${network.slug}${binary ? ` (binary: ${binary})` : ''}`);
|
|
379
|
+
|
|
380
|
+
let idleCount = 0;
|
|
381
|
+
|
|
382
|
+
while (!this._shuttingDown && !this._stoppedAgents.has(name)) {
|
|
383
|
+
try {
|
|
384
|
+
const { messages, cursor: newCursor } = await wsClient.pollPending(
|
|
385
|
+
network.id, name, network.token, { after: cursor }
|
|
386
|
+
);
|
|
387
|
+
if (newCursor) cursor = newCursor;
|
|
388
|
+
|
|
389
|
+
// Filter already-processed messages
|
|
390
|
+
const incoming = messages.filter((m) => {
|
|
391
|
+
const id = m.messageId;
|
|
392
|
+
if (!id || processedIds.has(id)) return false;
|
|
393
|
+
if (m.messageType === 'status') return false;
|
|
394
|
+
return true;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (incoming.length > 0) {
|
|
398
|
+
idleCount = 0;
|
|
399
|
+
for (const msg of incoming) {
|
|
400
|
+
if (msg.messageId) processedIds.add(msg.messageId);
|
|
401
|
+
await this._handleAdapterMessage(name, agentCfg, msg, network, wsClient, binary, env);
|
|
402
|
+
}
|
|
403
|
+
// Cap dedup set
|
|
404
|
+
if (processedIds.size > 2000) {
|
|
405
|
+
const arr = [...processedIds];
|
|
406
|
+
processedIds.clear();
|
|
407
|
+
for (const id of arr.slice(-1000)) processedIds.add(id);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
idleCount++;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Adaptive polling: 2s active, up to 15s idle
|
|
414
|
+
const delay = incoming.length > 0 ? 2000 : Math.min(2000 + idleCount * 1000, 15000);
|
|
415
|
+
await this._sleep(delay);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
if (this._stoppedAgents.has(name) || this._shuttingDown) break;
|
|
418
|
+
info.lastError = (e.message || String(e)).slice(0, 200);
|
|
419
|
+
this._log(`${name} poll error: ${info.lastError}`);
|
|
420
|
+
await this._sleep(5000);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
clearInterval(heartbeatInterval);
|
|
425
|
+
|
|
426
|
+
// Disconnect from workspace
|
|
427
|
+
try { await wsClient.disconnect(network.id, name, network.token); } catch {}
|
|
428
|
+
|
|
429
|
+
info.state = 'stopped';
|
|
430
|
+
this._writeStatus();
|
|
431
|
+
this._log(`${name} adapter stopped`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async _handleAdapterMessage(name, agentCfg, msg, network, wsClient, binary, env) {
|
|
435
|
+
const content = (msg.content || '').trim();
|
|
436
|
+
if (!content) return;
|
|
437
|
+
|
|
438
|
+
const channel = msg.sessionId || 'general';
|
|
439
|
+
const sender = msg.senderName || msg.senderType || 'user';
|
|
440
|
+
this._log(`${name} message from ${sender} in ${channel}: ${content.slice(0, 80)}`);
|
|
441
|
+
|
|
442
|
+
// Send "thinking..." status
|
|
443
|
+
try {
|
|
444
|
+
await wsClient.sendMessage(network.id, channel, network.token, 'thinking...', {
|
|
445
|
+
senderType: 'agent', senderName: name, messageType: 'status',
|
|
446
|
+
});
|
|
447
|
+
} catch {}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
let response;
|
|
451
|
+
if (binary) {
|
|
452
|
+
response = await this._runCliAgent(binary, content, channel, agentCfg, network, env);
|
|
453
|
+
} else {
|
|
454
|
+
response = `Agent type '${agentCfg.type}' has no CLI binary — cannot process message.`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (response) {
|
|
458
|
+
await wsClient.sendMessage(network.id, channel, network.token, response, {
|
|
459
|
+
senderType: 'agent', senderName: name,
|
|
460
|
+
});
|
|
461
|
+
this._log(`${name} responded in ${channel}: ${response.slice(0, 80)}...`);
|
|
462
|
+
}
|
|
463
|
+
} catch (e) {
|
|
464
|
+
const errMsg = `Error: ${(e.message || String(e)).slice(0, 200)}`;
|
|
465
|
+
this._log(`${name} error handling message: ${errMsg}`);
|
|
466
|
+
try {
|
|
467
|
+
await wsClient.sendMessage(network.id, channel, network.token, errMsg, {
|
|
468
|
+
senderType: 'agent', senderName: name,
|
|
469
|
+
});
|
|
470
|
+
} catch {}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
_runCliAgent(binary, message, channel, agentCfg, network, env) {
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
const sessionKey = `openagents-${network.id.slice(0, 8)}-${channel.slice(-8)}`;
|
|
477
|
+
const agentId = agentCfg.openclaw_agent_id || 'main';
|
|
478
|
+
|
|
479
|
+
const args = ['agent', '--local', '--agent', agentId,
|
|
480
|
+
'--session-id', sessionKey, '--message', message, '--json'];
|
|
481
|
+
|
|
482
|
+
this._log(`${agentCfg.name} CLI: ${binary} ${args.slice(0, 5).join(' ')} ...`);
|
|
483
|
+
|
|
484
|
+
const spawnEnv = { ...env };
|
|
485
|
+
if (IS_WINDOWS) {
|
|
486
|
+
const npmBin = path.join(process.env.APPDATA || '', 'npm');
|
|
487
|
+
if (npmBin && !(spawnEnv.PATH || '').includes(npmBin)) {
|
|
488
|
+
spawnEnv.PATH = npmBin + ';' + (spawnEnv.PATH || process.env.PATH || '');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// On Windows, .cmd shims need shell:true but that breaks argument
|
|
493
|
+
// quoting. Use execFileSync-style approach: resolve the .cmd to its
|
|
494
|
+
// target and run directly, or use spawn without shell and full path.
|
|
495
|
+
let spawnBinary = binary;
|
|
496
|
+
let spawnArgs = args;
|
|
497
|
+
const spawnOpts = {
|
|
498
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
499
|
+
env: spawnEnv,
|
|
500
|
+
timeout: 600000,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
if (IS_WINDOWS) {
|
|
504
|
+
// Find the actual .cmd shim and invoke via cmd.exe /C with proper quoting
|
|
505
|
+
spawnBinary = process.env.COMSPEC || 'cmd.exe';
|
|
506
|
+
// Wrap argument containing spaces in double quotes for cmd.exe
|
|
507
|
+
const quotedArgs = args.map((a) => a.includes(' ') ? `"${a}"` : a);
|
|
508
|
+
spawnArgs = ['/C', binary, ...quotedArgs];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const proc = spawn(spawnBinary, spawnArgs, spawnOpts);
|
|
512
|
+
let stdout = '';
|
|
513
|
+
let stderr = '';
|
|
514
|
+
|
|
515
|
+
if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d; });
|
|
516
|
+
if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
|
|
517
|
+
|
|
518
|
+
proc.on('error', (err) => reject(err));
|
|
519
|
+
proc.on('exit', (code) => {
|
|
520
|
+
if (code !== 0) {
|
|
521
|
+
reject(new Error(`CLI exited ${code}: ${stderr.slice(0, 300)}`));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
stdout = stdout.trim();
|
|
526
|
+
if (!stdout) { resolve(''); return; }
|
|
527
|
+
|
|
528
|
+
// Parse JSON output — find first '{'
|
|
529
|
+
const jsonStart = stdout.indexOf('{');
|
|
530
|
+
if (jsonStart < 0) { resolve(stdout); return; }
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const data = JSON.parse(stdout.slice(jsonStart));
|
|
534
|
+
const payloads = data.payloads || [];
|
|
535
|
+
if (payloads.length > 0) {
|
|
536
|
+
const texts = payloads.filter((p) => p.text).map((p) => p.text);
|
|
537
|
+
resolve(texts.join('\n\n'));
|
|
538
|
+
} else {
|
|
539
|
+
resolve(stdout.slice(0, jsonStart).trim() || '');
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
resolve(stdout);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
_resolveAgentBinary(agentCfg) {
|
|
549
|
+
const entry = this.registry.getEntry(agentCfg.type);
|
|
550
|
+
let binary = (entry && entry.install && entry.install.binary);
|
|
551
|
+
if (!binary) {
|
|
552
|
+
const knownBinaries = {
|
|
553
|
+
openclaw: 'openclaw', claude: 'claude', codex: 'codex',
|
|
554
|
+
aider: 'aider', goose: 'goose', gemini: 'gemini',
|
|
555
|
+
};
|
|
556
|
+
binary = knownBinaries[agentCfg.type];
|
|
557
|
+
}
|
|
558
|
+
return binary || null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
// Internal — spawn loop (local-only agents)
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
|
|
336
565
|
_spawnAgent(cmd, opts) {
|
|
337
566
|
const [binary, ...args] = cmd;
|
|
338
567
|
const spawnOpts = {
|
|
@@ -341,8 +570,14 @@ class Daemon {
|
|
|
341
570
|
cwd: opts.cwd,
|
|
342
571
|
};
|
|
343
572
|
|
|
344
|
-
if (IS_WINDOWS
|
|
573
|
+
if (IS_WINDOWS) {
|
|
574
|
+
// On Windows, always use shell so .cmd/.ps1 shims on PATH are found
|
|
345
575
|
spawnOpts.shell = true;
|
|
576
|
+
// Ensure npm global bin is on PATH
|
|
577
|
+
const npmBin = path.join(process.env.APPDATA || '', 'npm');
|
|
578
|
+
if (npmBin && !(process.env.PATH || '').includes(npmBin)) {
|
|
579
|
+
spawnOpts.env = { ...spawnOpts.env, PATH: npmBin + ';' + (spawnOpts.env.PATH || process.env.PATH || '') };
|
|
580
|
+
}
|
|
346
581
|
}
|
|
347
582
|
|
|
348
583
|
const proc = spawn(binary, args, spawnOpts);
|
|
@@ -358,19 +593,29 @@ class Daemon {
|
|
|
358
593
|
}
|
|
359
594
|
|
|
360
595
|
_getLaunchCommand(agentCfg) {
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
596
|
+
const binary = this._resolveAgentBinary(agentCfg);
|
|
597
|
+
if (!binary) return null;
|
|
363
598
|
|
|
364
|
-
const
|
|
599
|
+
const entry = this.registry.getEntry(agentCfg.type);
|
|
365
600
|
const args = [];
|
|
366
601
|
|
|
367
602
|
// Add launch args from registry
|
|
368
|
-
if (entry.launch && entry.launch.args) {
|
|
603
|
+
if (entry && entry.launch && entry.launch.args) {
|
|
369
604
|
for (const arg of entry.launch.args) {
|
|
370
605
|
args.push(arg.replace(/\{agent_name\}/g, agentCfg.name));
|
|
371
606
|
}
|
|
372
607
|
}
|
|
373
608
|
|
|
609
|
+
// Built-in launch profiles for local-only agents
|
|
610
|
+
if (!args.length) {
|
|
611
|
+
const type = agentCfg.type || '';
|
|
612
|
+
if (type === 'claude') {
|
|
613
|
+
args.push('--print');
|
|
614
|
+
} else if (type === 'codex') {
|
|
615
|
+
args.push('--quiet');
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
374
619
|
return [binary, ...args];
|
|
375
620
|
}
|
|
376
621
|
|
package/src/workspace-client.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const http = require('http');
|
|
5
5
|
|
|
6
|
-
const DEFAULT_ENDPOINT = 'https://endpoint.openagents.org';
|
|
6
|
+
const DEFAULT_ENDPOINT = 'https://workspace-endpoint.openagents.org';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* HTTP client for workspace API operations.
|
|
@@ -143,6 +143,79 @@ class WorkspaceClient {
|
|
|
143
143
|
}, token);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Poll for pending messages targeted at an agent via GET /v1/events.
|
|
148
|
+
* Returns { messages, cursor } where cursor is the last event ID.
|
|
149
|
+
*/
|
|
150
|
+
async pollPending(workspaceId, agentName, token, { after, limit = 50 } = {}) {
|
|
151
|
+
const params = new URLSearchParams({
|
|
152
|
+
network: workspaceId,
|
|
153
|
+
type: 'workspace.message',
|
|
154
|
+
limit: String(limit),
|
|
155
|
+
});
|
|
156
|
+
if (after) params.set('after', after);
|
|
157
|
+
|
|
158
|
+
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
159
|
+
const result = data.data || data;
|
|
160
|
+
const events = (result && result.events) || [];
|
|
161
|
+
|
|
162
|
+
let cursor = null;
|
|
163
|
+
if (events.length > 0) {
|
|
164
|
+
cursor = events[events.length - 1].id || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Filter for messages targeted at this agent
|
|
168
|
+
const messages = [];
|
|
169
|
+
for (const e of events) {
|
|
170
|
+
const source = e.source || '';
|
|
171
|
+
const meta = e.metadata || {};
|
|
172
|
+
const targetAgents = meta.target_agents || [];
|
|
173
|
+
|
|
174
|
+
// Skip own messages
|
|
175
|
+
if (source === `openagents:${agentName}`) continue;
|
|
176
|
+
|
|
177
|
+
if (source.startsWith('human:')) {
|
|
178
|
+
// Human messages: pick up if targeted at this agent or broadcast
|
|
179
|
+
if (!targetAgents.length || targetAgents.includes(agentName)) {
|
|
180
|
+
messages.push(this._eventToMessage(e));
|
|
181
|
+
}
|
|
182
|
+
} else if (source.startsWith('openagents:')) {
|
|
183
|
+
// Agent messages: only pick up if explicitly mentioned
|
|
184
|
+
if (targetAgents.includes(agentName)) {
|
|
185
|
+
messages.push(this._eventToMessage(e));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { messages, cursor };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Convert an ONM event to a message-compatible object.
|
|
195
|
+
*/
|
|
196
|
+
_eventToMessage(event) {
|
|
197
|
+
const source = event.source || '';
|
|
198
|
+
const isHuman = source.startsWith('human:');
|
|
199
|
+
const senderName = source.replace('openagents:', '').replace('human:', '');
|
|
200
|
+
const payload = event.payload || {};
|
|
201
|
+
const target = event.target || '';
|
|
202
|
+
const ts = event.timestamp;
|
|
203
|
+
|
|
204
|
+
const msg = {
|
|
205
|
+
messageId: event.id || '',
|
|
206
|
+
sessionId: target.startsWith('channel/') ? target.replace('channel/', '') : target,
|
|
207
|
+
senderType: isHuman ? 'human' : 'agent',
|
|
208
|
+
senderName,
|
|
209
|
+
content: (payload.content || ''),
|
|
210
|
+
messageType: payload.message_type || 'chat',
|
|
211
|
+
metadata: event.metadata || {},
|
|
212
|
+
};
|
|
213
|
+
if (ts) {
|
|
214
|
+
msg.createdAt = new Date(ts).toISOString();
|
|
215
|
+
}
|
|
216
|
+
return msg;
|
|
217
|
+
}
|
|
218
|
+
|
|
146
219
|
// -- Internal --
|
|
147
220
|
|
|
148
221
|
_wsHeaders(token) {
|
|
@@ -152,6 +225,40 @@ class WorkspaceClient {
|
|
|
152
225
|
};
|
|
153
226
|
}
|
|
154
227
|
|
|
228
|
+
_get(urlPath, headers = {}) {
|
|
229
|
+
const fullUrl = this.endpoint + urlPath;
|
|
230
|
+
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const parsedUrl = new URL(fullUrl);
|
|
233
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
234
|
+
|
|
235
|
+
const req = transport.request(fullUrl, {
|
|
236
|
+
method: 'GET',
|
|
237
|
+
headers,
|
|
238
|
+
timeout: 15000,
|
|
239
|
+
}, (res) => {
|
|
240
|
+
let data = '';
|
|
241
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
242
|
+
res.on('end', () => {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(data);
|
|
245
|
+
if (res.statusCode >= 400) {
|
|
246
|
+
reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
|
|
247
|
+
} else {
|
|
248
|
+
resolve(parsed);
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
req.on('error', reject);
|
|
257
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
258
|
+
req.end();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
155
262
|
_post(urlPath, body, headers = {}) {
|
|
156
263
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
157
264
|
const jsonBody = JSON.stringify(body);
|