@openagents-org/agent-connector 0.1.11 → 0.2.1

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/src/daemon.js CHANGED
@@ -339,316 +339,63 @@ class Daemon {
339
339
  // ---------------------------------------------------------------------------
340
340
 
341
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();
342
+ const { createAdapter } = require('./adapters');
343
+ const agentType = agentCfg.type || 'openclaw';
344
+ const endpoint = network.endpoint || 'https://workspace-endpoint.openagents.org';
347
345
 
348
- // Skip existing events on startup
346
+ let adapter;
349
347
  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}`);
348
+ adapter = createAdapter(agentType, {
349
+ workspaceId: network.id,
350
+ channelName: 'general',
351
+ token: network.token,
352
+ agentName: name,
353
+ endpoint,
354
+ openclawAgentId: agentCfg.openclaw_agent_id || 'main',
355
+ disabledModules: new Set(),
356
+ });
358
357
  } catch (e) {
359
- this._log(`${name} skip-events failed: ${e.message}`);
358
+ this._log(`${name} failed to create ${agentType} adapter: ${e.message}`);
359
+ info.state = 'error';
360
+ info.lastError = e.message;
361
+ this._writeStatus();
362
+ return;
360
363
  }
361
364
 
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
- // Install workspace skill into OpenClaw's skills directory
376
- try { this._installWorkspaceSkill(agentCfg, network); } catch (e) {
377
- this._log(`${name} skill install failed: ${e.message}`);
378
- }
365
+ // Store adapter reference for stop
366
+ this._adapters = this._adapters || {};
367
+ this._adapters[name] = adapter;
379
368
 
380
369
  info.state = 'running';
381
370
  info.startedAt = new Date().toISOString();
382
371
  this._writeStatus();
383
- this._log(`${name} adapter online → ${network.slug}${binary ? ` (binary: ${binary})` : ''}`);
372
+ this._log(`${name} adapter online → ${network.slug} (type: ${agentType})`);
384
373
 
385
- let idleCount = 0;
386
-
387
- while (!this._shuttingDown && !this._stoppedAgents.has(name)) {
388
- try {
389
- const { messages, cursor: newCursor } = await wsClient.pollPending(
390
- network.id, name, network.token, { after: cursor }
391
- );
392
- if (newCursor) cursor = newCursor;
393
-
394
- // Filter already-processed messages
395
- const incoming = messages.filter((m) => {
396
- const id = m.messageId;
397
- if (!id || processedIds.has(id)) return false;
398
- if (m.messageType === 'status') return false;
399
- return true;
400
- });
401
-
402
- if (incoming.length > 0) {
403
- idleCount = 0;
404
- for (const msg of incoming) {
405
- if (msg.messageId) processedIds.add(msg.messageId);
406
- await this._handleAdapterMessage(name, agentCfg, msg, network, wsClient, binary, env);
407
- }
408
- // Cap dedup set
409
- if (processedIds.size > 2000) {
410
- const arr = [...processedIds];
411
- processedIds.clear();
412
- for (const id of arr.slice(-1000)) processedIds.add(id);
413
- }
414
- } else {
415
- idleCount++;
374
+ try {
375
+ // Run adapter poll loop — stops when adapter.stop() is called
376
+ // or when the daemon shuts down
377
+ const checkStop = setInterval(() => {
378
+ if (this._shuttingDown || this._stoppedAgents.has(name)) {
379
+ adapter.stop();
380
+ clearInterval(checkStop);
416
381
  }
382
+ }, 1000);
417
383
 
418
- // Adaptive polling: 2s active, up to 15s idle
419
- const delay = incoming.length > 0 ? 2000 : Math.min(2000 + idleCount * 1000, 15000);
420
- await this._sleep(delay);
421
- } catch (e) {
422
- if (this._stoppedAgents.has(name) || this._shuttingDown) break;
423
- info.lastError = (e.message || String(e)).slice(0, 200);
424
- this._log(`${name} poll error: ${info.lastError}`);
425
- await this._sleep(5000);
426
- }
384
+ await adapter.run();
385
+ clearInterval(checkStop);
386
+ } catch (e) {
387
+ info.lastError = (e.message || String(e)).slice(0, 200);
388
+ this._log(`${name} adapter error: ${info.lastError}`);
427
389
  }
428
390
 
429
- clearInterval(heartbeatInterval);
430
-
431
- // Disconnect from workspace
432
- try { await wsClient.disconnect(network.id, name, network.token); } catch {}
433
-
391
+ delete this._adapters[name];
434
392
  info.state = 'stopped';
435
393
  this._writeStatus();
436
394
  this._log(`${name} adapter stopped`);
437
395
  }
438
396
 
439
- async _handleAdapterMessage(name, agentCfg, msg, network, wsClient, binary, env) {
440
- const content = (msg.content || '').trim();
441
- if (!content) return;
442
-
443
- const channel = msg.sessionId || 'general';
444
- const sender = msg.senderName || msg.senderType || 'user';
445
- this._log(`${name} message from ${sender} in ${channel}: ${content.slice(0, 80)}`);
446
-
447
- // Send "thinking..." status
448
- try {
449
- await wsClient.sendMessage(network.id, channel, network.token, 'thinking...', {
450
- senderType: 'agent', senderName: name, messageType: 'status',
451
- });
452
- } catch {}
453
-
454
- try {
455
- let response;
456
- if (binary) {
457
- response = await this._runCliAgent(binary, content, channel, agentCfg, network, env);
458
- } else {
459
- response = `Agent type '${agentCfg.type}' has no CLI binary — cannot process message.`;
460
- }
461
-
462
- if (response) {
463
- await wsClient.sendMessage(network.id, channel, network.token, response, {
464
- senderType: 'agent', senderName: name,
465
- });
466
- this._log(`${name} responded in ${channel}: ${response.slice(0, 80)}...`);
467
- }
468
- } catch (e) {
469
- const errMsg = `Error: ${(e.message || String(e)).slice(0, 200)}`;
470
- this._log(`${name} error handling message: ${errMsg}`);
471
- try {
472
- await wsClient.sendMessage(network.id, channel, network.token, errMsg, {
473
- senderType: 'agent', senderName: name,
474
- });
475
- } catch {}
476
- }
477
- }
478
-
479
- /**
480
- * Build workspace context preamble for CLI agents.
481
- * Teaches the agent about shared workspace APIs (browser, files).
482
- */
483
- /**
484
- * On Windows, resolve a .cmd shim to the underlying node script
485
- * so we can spawn directly with node (avoiding cmd.exe argument limits).
486
- */
487
- _resolveWindowsBinary(binary, env) {
488
- const { execSync } = require('child_process');
489
- try {
490
- // Find the .cmd shim
491
- const cmdPath = execSync(`where ${binary}`, {
492
- encoding: 'utf-8', timeout: 5000, env,
493
- }).split(/\r?\n/)[0].trim();
494
-
495
- if (cmdPath.endsWith('.cmd')) {
496
- // Read the .cmd file to find the target JS script
497
- const cmdContent = fs.readFileSync(cmdPath, 'utf-8');
498
- // npm .cmd shims have: "%~dp0\node_modules\...\bin\cli.js" %*
499
- // or: @IF EXIST "%~dp0\node.exe" ... "%~dp0\node_modules\...\cli.js" %*
500
- const match = cmdContent.match(/"([^"]+\.js)"/);
501
- if (match) {
502
- const jsPath = match[1].replace('%~dp0\\', path.dirname(cmdPath) + '\\');
503
- return { binary: process.execPath, prefix: [jsPath] };
504
- }
505
- }
506
- } catch {}
507
-
508
- // Fallback: use cmd.exe /C (may truncate long args)
509
- return { binary: process.env.COMSPEC || 'cmd.exe', prefix: ['/C', binary] };
510
- }
511
-
512
- /**
513
- * Install a SKILL.md into OpenClaw's workspace skills directory.
514
- * OpenClaw auto-discovers skills from <workspace>/skills/ and injects
515
- * them into the system prompt — same mechanism as the Python adapter.
516
- */
517
- _installWorkspaceSkill(agentCfg, network) {
518
- const baseUrl = 'https://workspace-endpoint.openagents.org';
519
- const h = `Authorization: Bearer ${network.token}`;
520
- const wsId = network.id;
521
- const name = agentCfg.name;
522
- const agentId = agentCfg.openclaw_agent_id || 'main';
523
-
524
- const home = IS_WINDOWS ? process.env.USERPROFILE : process.env.HOME;
525
- const openclawDir = path.join(home, '.openclaw');
526
- const wsDir = agentId && agentId !== 'main'
527
- ? path.join(openclawDir, `workspace-${agentId}`)
528
- : path.join(openclawDir, 'workspace');
529
-
530
- if (!fs.existsSync(wsDir)) {
531
- this._log(`OpenClaw workspace not found at ${wsDir}, skipping skill install`);
532
- return;
533
- }
534
-
535
- const skillName = `openagents-workspace-${name}`;
536
- const skillDir = path.join(wsDir, 'skills', skillName);
537
- fs.mkdirSync(skillDir, { recursive: true });
538
-
539
- const content = [
540
- '---',
541
- 'name: openagents-workspace',
542
- 'description: "Share files, browse websites, and collaborate with other agents in an OpenAgents workspace. Use when: (1) sharing results or reports, (2) browsing a website, (3) reading shared files, (4) checking who else is in the workspace."',
543
- 'metadata:',
544
- ' {"openclaw": {"always": true, "emoji": "\\U0001F310"}}',
545
- '---',
546
- '',
547
- `You are agent "${name}" in workspace "${network.slug || network.name || wsId}".`,
548
- '',
549
- '## Workspace Tools (MANDATORY)',
550
- '',
551
- 'Use your `exec` tool to run the `curl` commands below.',
552
- 'Do NOT use native browsing tools for workspace browser tasks — use `exec` + `curl`.',
553
- 'The workspace browser is a *shared* browser visible to all users and agents.',
554
- '',
555
- '### Shared Browser',
556
- '',
557
- '**To browse a website**, exec these steps:',
558
- `Step 1 — open tab: curl -s -X POST ${baseUrl}/v1/browser/tabs -H "${h}" -H "Content-Type: application/json" -d '{"url":"https://example.com","network":"${wsId}","source":"openagents:${name}"}'`,
559
- `Step 2 — read content: curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID/snapshot`,
560
- `Step 3 — close tab: curl -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID`,
561
- '(Replace TAB_ID with the id from step 1 response)',
562
- '',
563
- `List tabs: \`curl -s -H "${h}" ${baseUrl}/v1/browser/tabs?network=${wsId}\``,
564
- `Navigate: \`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ${baseUrl}/v1/browser/tabs/{tab_id}/navigate -d '{"url":"URL"}'\``,
565
- `Click: \`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ${baseUrl}/v1/browser/tabs/{tab_id}/click -d '{"selector":"CSS"}'\``,
566
- `Type: \`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ${baseUrl}/v1/browser/tabs/{tab_id}/type -d '{"selector":"CSS","text":"TEXT"}'\``,
567
- `Close: \`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\``,
568
- '',
569
- '### Workspace Files',
570
- '',
571
- `List: \`curl -s -H "${h}" ${baseUrl}/v1/files?network=${wsId}\``,
572
- `Read: \`curl -s -H "${h}" ${baseUrl}/v1/files/FILE_PATH?network=${wsId}\``,
573
- `Write: \`curl -s -X PUT -H "${h}" -H "Content-Type: application/json" ${baseUrl}/v1/files/FILE_PATH -d '{"content":"...","network":"${wsId}"}'\``,
574
- '',
575
- ].join('\n');
576
-
577
- const skillPath = path.join(skillDir, 'SKILL.md');
578
- fs.writeFileSync(skillPath, content, 'utf-8');
579
- this._log(`Installed workspace skill at ${skillPath}`);
580
- }
581
-
582
- _runCliAgent(binary, message, channel, agentCfg, network, env) {
583
- return new Promise((resolve, reject) => {
584
- const sessionKey = `openagents-${network.id.slice(0, 8)}-${channel.slice(-8)}`;
585
- const agentId = agentCfg.openclaw_agent_id || 'main';
586
-
587
- const args = ['agent', '--local', '--agent', agentId,
588
- '--session-id', sessionKey, '--message', message, '--json'];
589
-
590
- this._log(`${agentCfg.name} CLI: ${binary} ${args.slice(0, 5).join(' ')} ...`);
591
-
592
- const spawnEnv = { ...env };
593
- if (IS_WINDOWS) {
594
- const npmBin = path.join(process.env.APPDATA || '', 'npm');
595
- if (npmBin && !(spawnEnv.PATH || '').includes(npmBin)) {
596
- spawnEnv.PATH = npmBin + ';' + (spawnEnv.PATH || process.env.PATH || '');
597
- }
598
- }
599
-
600
- // On Windows, use cmd.exe /C for .cmd shim resolution
601
- let spawnBinary = binary;
602
- let spawnArgs = args;
603
- const spawnOpts = {
604
- stdio: ['ignore', 'pipe', 'pipe'],
605
- env: spawnEnv,
606
- timeout: 600000,
607
- };
608
-
609
- if (IS_WINDOWS) {
610
- spawnBinary = process.env.COMSPEC || 'cmd.exe';
611
- const quotedArgs = args.map((a) => a.includes(' ') ? `"${a}"` : a);
612
- spawnArgs = ['/C', binary, ...quotedArgs];
613
- }
614
-
615
-
616
- const proc = spawn(spawnBinary, spawnArgs, spawnOpts);
617
- let stdout = '';
618
- let stderr = '';
619
-
620
- if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d; });
621
- if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
622
-
623
- proc.on('error', (err) => reject(err));
624
- proc.on('exit', (code) => {
625
- if (code !== 0) {
626
- reject(new Error(`CLI exited ${code}: ${stderr.slice(0, 300)}`));
627
- return;
628
- }
629
-
630
- stdout = stdout.trim();
631
- if (!stdout) { resolve(''); return; }
632
-
633
- // Parse JSON output — find first '{'
634
- const jsonStart = stdout.indexOf('{');
635
- if (jsonStart < 0) { resolve(stdout); return; }
636
-
637
- try {
638
- const data = JSON.parse(stdout.slice(jsonStart));
639
- const payloads = data.payloads || [];
640
- if (payloads.length > 0) {
641
- const texts = payloads.filter((p) => p.text).map((p) => p.text);
642
- resolve(texts.join('\n\n'));
643
- } else {
644
- resolve(stdout.slice(0, jsonStart).trim() || '');
645
- }
646
- } catch {
647
- resolve(stdout);
648
- }
649
- });
650
- });
651
- }
397
+ // NOTE: Adapter-specific message handling (openclaw, claude, codex)
398
+ // has been moved to src/adapters/. The daemon delegates via createAdapter().
652
399
 
653
400
  _resolveAgentBinary(agentCfg) {
654
401
  const entry = this.registry.getEntry(agentCfg.type);
package/src/index.js CHANGED
@@ -195,4 +195,6 @@ class AgentConnector {
195
195
  }
196
196
  }
197
197
 
198
- module.exports = { AgentConnector, Daemon, WorkspaceClient };
198
+ const adapters = require('./adapters');
199
+
200
+ module.exports = { AgentConnector, Daemon, WorkspaceClient, adapters };
@@ -105,17 +105,6 @@ class WorkspaceClient {
105
105
  } catch {}
106
106
  }
107
107
 
108
- /**
109
- * Poll for pending tasks via POST /v1/poll_pending.
110
- */
111
- async pollPending(workspaceId, agentName, token) {
112
- const data = await this._post('/v1/poll_pending', {
113
- agent_name: agentName,
114
- network: workspaceId,
115
- }, this._wsHeaders(token));
116
- return data.data || data;
117
- }
118
-
119
108
  /**
120
109
  * Post a task result via POST /v1/events.
121
110
  */
@@ -190,6 +179,54 @@ class WorkspaceClient {
190
179
  return { messages, cursor };
191
180
  }
192
181
 
182
+ /**
183
+ * Get session/channel info via GET /v1/sessions/{channelName}.
184
+ */
185
+ async getSession(workspaceId, channelName, token) {
186
+ try {
187
+ const params = new URLSearchParams({ network: workspaceId });
188
+ const data = await this._get(`/v1/sessions/${channelName}?${params}`, this._wsHeaders(token));
189
+ return (data.data || data) || {};
190
+ } catch {
191
+ return {};
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Update session/channel info via PUT /v1/sessions/{channelName}.
197
+ */
198
+ async updateSession(workspaceId, channelName, token, { title, autoTitle } = {}) {
199
+ const body = { network: workspaceId };
200
+ if (title !== undefined) body.title = title;
201
+ if (autoTitle !== undefined) body.auto_title = autoTitle;
202
+ try {
203
+ await this._post(`/v1/sessions/${channelName}`, body, this._wsHeaders(token));
204
+ } catch {}
205
+ }
206
+
207
+ /**
208
+ * Poll for control events targeted at an agent via GET /v1/events.
209
+ */
210
+ async pollControl(workspaceId, agentName, token, { after } = {}) {
211
+ try {
212
+ const params = new URLSearchParams({
213
+ network: workspaceId,
214
+ type: 'workspace.control',
215
+ limit: '10',
216
+ });
217
+ if (after) params.set('after', after);
218
+ const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
219
+ const result = data.data || data;
220
+ const events = (result && result.events) || [];
221
+ return events.filter((e) => {
222
+ const targets = (e.metadata || {}).target_agents || [];
223
+ return !targets.length || targets.includes(agentName);
224
+ });
225
+ } catch {
226
+ return [];
227
+ }
228
+ }
229
+
193
230
  /**
194
231
  * Convert an ONM event to a message-compatible object.
195
232
  */