@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 +1 -1
- package/registry.json +18 -23
- package/src/adapters/base.js +137 -5
- package/src/adapters/claude.js +1 -1
- package/src/adapters/cursor.js +599 -9
- package/src/adapters/utils.js +34 -10
- package/src/adapters/workspace-prompt.js +199 -66
- package/src/mcp-server.js +8 -1
- package/src/workspace-client.js +27 -0
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -209,49 +209,44 @@
|
|
|
209
209
|
"order": 4,
|
|
210
210
|
"builtin": true,
|
|
211
211
|
"install": {
|
|
212
|
-
"binary": "
|
|
213
|
-
"verify": "
|
|
214
|
-
"verify_win": "
|
|
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": "
|
|
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": "
|
|
234
|
-
"description": "API key for
|
|
235
|
-
"required":
|
|
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
|
-
"
|
|
241
|
-
|
|
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": "
|
|
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",
|
package/src/adapters/base.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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;
|
package/src/adapters/claude.js
CHANGED
|
@@ -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
|
}
|