@openagents-org/agent-launcher 0.2.127 → 0.2.130

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.127",
3
+ "version": "0.2.130",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/registry.json CHANGED
@@ -209,49 +209,44 @@
209
209
  "order": 4,
210
210
  "builtin": true,
211
211
  "install": {
212
- "binary": "cursor",
213
- "verify": "cursor --version 2>/dev/null | grep -qi cursor",
214
- "verify_win": "cursor --version 2>nul | findstr /i cursor",
215
- "requires": [
216
- null
217
- ],
212
+ "binary": "agent",
213
+ "verify": "agent --version 2>/dev/null | head -1",
214
+ "verify_win": "agent --version 2>nul",
215
+ "requires": [],
218
216
  "macos": "curl https://cursor.com/install -fsSL | bash",
219
217
  "linux": "curl https://cursor.com/install -fsSL | bash",
220
- "windows": "echo 'Download Cursor from https://cursor.com/downloads'"
218
+ "windows": "irm 'https://cursor.com/install?win32=true' | iex",
219
+ "npm": "npm install -g @cursor/cli"
221
220
  },
222
221
  "adapter": {
223
222
  "module": "openagents.adapters.cursor",
224
223
  "class": "CursorAdapter"
225
224
  },
226
225
  "launch": {
227
- "args": [
228
- null
229
- ]
226
+ "args": []
230
227
  },
231
228
  "env_config": [
232
229
  {
233
- "name": "OPENAI_API_KEY",
234
- "description": "API key for LLM inference",
235
- "required": true,
230
+ "name": "CURSOR_API_KEY",
231
+ "description": "Cursor API key for CLI authentication",
232
+ "required": false,
236
233
  "password": true
234
+ },
235
+ {
236
+ "name": "CURSOR_MODEL",
237
+ "description": "Model to use (e.g. claude-sonnet-4-6, gpt-4o)",
238
+ "required": false
237
239
  }
238
240
  ],
239
241
  "check_ready": {
240
- "env_vars": [
241
- "OPENAI_API_KEY"
242
- ],
243
- "saved_env_key": "OPENAI_API_KEY",
244
- "not_ready_message": "No API key \u2014 press e to configure"
242
+ "binary": "agent",
243
+ "not_ready_message": "Cursor CLI not found \u2014 install with: curl https://cursor.com/install -fsSL | bash"
245
244
  },
246
245
  "resolve_env": {
247
246
  "rules": [
248
247
  {
249
248
  "from": "LLM_API_KEY",
250
- "to": "OPENAI_API_KEY"
251
- },
252
- {
253
- "from": "LLM_BASE_URL",
254
- "to": "OPENAI_BASE_URL"
249
+ "to": "CURSOR_API_KEY"
255
250
  },
256
251
  {
257
252
  "from": "LLM_MODEL",
@@ -42,6 +42,7 @@ class BaseAdapter {
42
42
  this.workingDir = workingDir || undefined;
43
43
  this.client = new WorkspaceClient(this.endpoint);
44
44
  this._lastEventId = null;
45
+ this._lastToolResultId = null;
45
46
  this._running = false;
46
47
  this._sessionId = null; // issued by server on /v1/join; used to prove liveness
47
48
  this._processedIds = new Set();
@@ -330,6 +331,14 @@ class BaseAdapter {
330
331
  const msgId = msg.id || msg.messageId;
331
332
  if (msgId && this._processedIds.has(msgId)) continue;
332
333
  if (msg.messageType === 'status') continue;
334
+ // Handle queue cancellation signals from frontend
335
+ if (msg.messageType === 'queue_cancel') {
336
+ if (msgId) this._processedIds.add(msgId);
337
+ const channel = msg.sessionId || this.channelName || 'general';
338
+ const queueId = msg.metadata?.queue_id || (msg.content || '').replace('__queue_cancel:', '');
339
+ if (queueId) this._cancelQueuedMessage(channel, queueId);
340
+ continue;
341
+ }
333
342
  incoming.push(msg);
334
343
  }
335
344
 
@@ -350,6 +359,31 @@ class BaseAdapter {
350
359
  idleCount++;
351
360
  }
352
361
 
362
+ // Sidecar poll: A2UI tool_result events. These are the user's response
363
+ // to a UI spec this agent (or any agent in the network) emitted. We
364
+ // surface each one as a synthetic user message so the LLM sees it as
365
+ // the next turn and can react. Failures here don't break the main
366
+ // message poll.
367
+ try {
368
+ const toolResult = await this.client.pollToolResults(
369
+ this.workspaceId, this.token,
370
+ { after: this._lastToolResultId }
371
+ );
372
+ if (toolResult.cursor) this._lastToolResultId = toolResult.cursor;
373
+ for (const event of toolResult.events || []) {
374
+ const msgId = event.id;
375
+ if (msgId && this._processedIds.has(msgId)) continue;
376
+ if (msgId) this._processedIds.add(msgId);
377
+ const synth = synthesizeToolResultMessage(event);
378
+ if (synth) await this._dispatchMessage(synth);
379
+ }
380
+ } catch (e) {
381
+ // Non-fatal — log once per poll if it fails
382
+ if (pollCount <= 3 || pollCount % 20 === 0) {
383
+ this._log(`tool_result poll #${pollCount} failed: ${e.message}`);
384
+ }
385
+ }
386
+
353
387
  // Adaptive polling: 2s active, up to 15s idle.
354
388
  // Each connected agent runs this loop, so faster rates multiply across
355
389
  // every workspace member — keep this conservative and tune separately
@@ -372,9 +406,14 @@ class BaseAdapter {
372
406
 
373
407
  if (this._channelBusy.has(channel)) {
374
408
  if (!this._channelQueues[channel]) this._channelQueues[channel] = [];
409
+ const queueId = `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
410
+ msg._queueId = queueId;
375
411
  this._channelQueues[channel].push(msg);
376
412
  try {
377
- await this.sendStatus(channel, 'message queued — will process after current task');
413
+ await this.sendStatus(channel, 'message queued — will process after current task', {
414
+ queued_message: (msg.content || '').slice(0, 200),
415
+ queue_id: queueId,
416
+ });
378
417
  } catch {}
379
418
  return;
380
419
  }
@@ -384,6 +423,16 @@ class BaseAdapter {
384
423
  this._wakeControlPoller();
385
424
  }
386
425
 
426
+ _cancelQueuedMessage(channel, queueId) {
427
+ const queue = this._channelQueues[channel];
428
+ if (!queue) return false;
429
+ const idx = queue.findIndex((m) => m._queueId === queueId);
430
+ if (idx === -1) return false;
431
+ queue.splice(idx, 1);
432
+ this._log(`Cancelled queued message ${queueId} in ${channel}`);
433
+ return true;
434
+ }
435
+
387
436
  async _channelWorker(channel, msg) {
388
437
  this._channelBusy.add(channel);
389
438
  try {
@@ -398,6 +447,9 @@ class BaseAdapter {
398
447
  const queue = this._channelQueues[channel];
399
448
  if (!queue || queue.length === 0) break;
400
449
  const nextMsg = queue.shift();
450
+ if (nextMsg._queueId) {
451
+ try { await this.sendStatus(channel, 'processing queued message', { queue_id: nextMsg._queueId, queue_status: 'processed' }); } catch {}
452
+ }
401
453
  try {
402
454
  await this._handleMessage(nextMsg);
403
455
  } catch (e) {
@@ -435,13 +487,13 @@ class BaseAdapter {
435
487
  // Message helpers
436
488
  // ------------------------------------------------------------------
437
489
 
438
- async sendStatus(channel, content) {
490
+ async sendStatus(channel, content, extraMeta) {
439
491
  try {
440
492
  await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
441
493
  senderType: 'agent',
442
494
  senderName: this.agentName,
443
495
  messageType: 'status',
444
- metadata: { agent_mode: this._mode },
496
+ metadata: { agent_mode: this._mode, ...extraMeta },
445
497
  sessionId: this._sessionId,
446
498
  });
447
499
  } catch (e) {
@@ -450,8 +502,15 @@ class BaseAdapter {
450
502
  }
451
503
 
452
504
  async sendThinking(channel, content) {
505
+ // Strip ```a2ui blocks if they leak into Claude's intermediate thinking
506
+ // trace — the real spec gets emitted via sendResponse with proper
507
+ // payload.spec extraction, so showing the raw block here is just noise
508
+ // (and a duplicate). If stripping leaves the thinking message empty,
509
+ // skip it entirely.
510
+ const { cleanContent } = extractA2UISpec(content);
511
+ if (!cleanContent || !cleanContent.trim()) return;
453
512
  try {
454
- await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
513
+ await this.client.sendMessage(this.workspaceId, channel, this.token, cleanContent, {
455
514
  senderType: 'agent',
456
515
  senderName: this.agentName,
457
516
  messageType: 'thinking',
@@ -464,11 +523,14 @@ class BaseAdapter {
464
523
  }
465
524
 
466
525
  async sendResponse(channel, content) {
526
+ const { cleanContent, spec, specToolCallId } = extractA2UISpec(content);
467
527
  try {
468
- await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
528
+ await this.client.sendMessage(this.workspaceId, channel, this.token, cleanContent, {
469
529
  senderType: 'agent',
470
530
  senderName: this.agentName,
471
531
  sessionId: this._sessionId,
532
+ spec,
533
+ specToolCallId,
472
534
  });
473
535
  } catch (e) {
474
536
  if (e instanceof SessionRevokedError) {
@@ -576,4 +638,74 @@ class BaseAdapter {
576
638
  }
577
639
  }
578
640
 
641
+ // ------------------------------------------------------------------
642
+ // A2UI helpers
643
+ // ------------------------------------------------------------------
644
+
645
+ /**
646
+ * Pull the first ```a2ui ... ``` fenced block out of LLM-produced content.
647
+ * Returns the content with the block stripped, the parsed spec, and a
648
+ * tool-call id derived from `spec.tool_call_id` (if present) or a new one.
649
+ * If no block is present or parsing fails, returns the content unchanged
650
+ * with null spec — the message still goes out as plain markdown.
651
+ */
652
+ /**
653
+ * Convert a workspace.tool_result event into a synthetic user-message
654
+ * shape that the agent's _handleMessage can dispatch. The LLM sees this
655
+ * as the next user turn — the content is a short, machine-readable line
656
+ * the LLM can parse without ambiguity. The original spec it emitted is
657
+ * already in the LLM's conversation history; the tool_call_id lets the
658
+ * LLM correlate this back.
659
+ */
660
+ function synthesizeToolResultMessage(event) {
661
+ if (!event || !event.payload) return null;
662
+ const p = event.payload;
663
+ const actionId = p.action_id || '';
664
+ const toolCallId = p.tool_call_id || '';
665
+ let valueStr = '';
666
+ if (p.value !== undefined && p.value !== null) {
667
+ try { valueStr = JSON.stringify(p.value); } catch (_) { valueStr = String(p.value); }
668
+ }
669
+ const lines = [
670
+ '[ui_action]',
671
+ `action=${actionId}`,
672
+ toolCallId ? `tool_call_id=${toolCallId}` : null,
673
+ valueStr ? `value=${valueStr}` : null,
674
+ ].filter(Boolean);
675
+ const content = lines.join(' ');
676
+ const target = event.target || '';
677
+ return {
678
+ messageId: event.id || '',
679
+ sessionId: target.startsWith('channel/') ? target.replace('channel/', '') : target,
680
+ senderType: 'human',
681
+ senderName: 'user',
682
+ content,
683
+ mentions: [],
684
+ messageType: 'chat',
685
+ metadata: event.metadata || {},
686
+ };
687
+ }
688
+
689
+ function extractA2UISpec(content) {
690
+ if (!content || typeof content !== 'string') {
691
+ return { cleanContent: content, spec: null, specToolCallId: null };
692
+ }
693
+ const match = content.match(/```a2ui\s*\n([\s\S]*?)\n```/);
694
+ if (!match) return { cleanContent: content, spec: null, specToolCallId: null };
695
+
696
+ let spec;
697
+ try {
698
+ spec = JSON.parse(match[1]);
699
+ } catch (_) {
700
+ return { cleanContent: content, spec: null, specToolCallId: null };
701
+ }
702
+
703
+ const specToolCallId = (spec && spec.tool_call_id) || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
704
+ if (spec && spec.tool_call_id) delete spec.tool_call_id;
705
+
706
+ const cleanContent = content.replace(match[0], '').trim();
707
+ return { cleanContent, spec, specToolCallId };
708
+ }
709
+
579
710
  module.exports = BaseAdapter;
711
+ module.exports.extractA2UISpec = extractA2UISpec;
@@ -569,7 +569,7 @@ class ClaudeAdapter extends BaseAdapter {
569
569
  let content = (msg.content || '').trim();
570
570
  const attachments = msg.attachments || [];
571
571
 
572
- const attText = formatAttachmentsForPrompt(attachments);
572
+ const attText = formatAttachmentsForPrompt(attachments, this.toolMode);
573
573
  if (attText) {
574
574
  content = content ? content + attText : attText.trim();
575
575
  }