@openagents-org/agent-launcher 0.2.128 → 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.
@@ -78,11 +78,12 @@ function buildModePrompt(mode) {
78
78
  *
79
79
  * In plan mode, only read-only operations are documented.
80
80
  */
81
- function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode = 'execute' }) {
81
+ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode = 'execute', isWindows = process.platform === 'win32' }) {
82
82
  const disabled = disabledModules || new Set();
83
83
  const baseUrl = endpoint.replace(/\/+$/, '');
84
84
  const isPlan = mode === 'plan';
85
85
  const h = `X-Workspace-Token: ${token}`;
86
+ const curl = isWindows ? 'curl.exe' : 'curl';
86
87
 
87
88
  const sections = [];
88
89
 
@@ -115,34 +116,53 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
115
116
  let s = '\n### Shared Files\n\n';
116
117
 
117
118
  if (!isPlan) {
118
- s += (
119
- '**To upload a file**, exec this (replace filename/content):\n' +
120
- `CONTENT=$(echo -n 'YOUR_CONTENT' | base64) && ` +
121
- `curl -s -X POST ${baseUrl}/v1/files/base64 ` +
122
- `-H "${h}" ` +
123
- '-H "Content-Type: application/json" ' +
124
- `-d '{"filename":"report.md",` +
125
- `"content_base64":"'"$CONTENT"'",` +
126
- `"content_type":"text/markdown",` +
127
- `"network":"${workspaceId}",` +
128
- `"source":"openagents:${agentName}",` +
129
- `"channel_name":"${channelName}"}'\n\n`
130
- );
119
+ if (isWindows) {
120
+ s += (
121
+ '**To upload a file**, exec this (replace filename/content):\n' +
122
+ `$CONTENT = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('YOUR_CONTENT'))\n` +
123
+ `${curl} -s -X POST ${baseUrl}/v1/files/base64 ` +
124
+ `-H "${h}" ` +
125
+ '-H "Content-Type: application/json" ' +
126
+ `-d "{\\"filename\\":\\"report.md\\",` +
127
+ `\\"content_base64\\":\\"$CONTENT\\",` +
128
+ `\\"content_type\\":\\"text/markdown\\",` +
129
+ `\\"network\\":\\"${workspaceId}\\",` +
130
+ `\\"source\\":\\"openagents:${agentName}\\",` +
131
+ `\\"channel_name\\":\\"${channelName}\\"}"\n\n`
132
+ );
133
+ } else {
134
+ s += (
135
+ '**To upload a file**, exec this (replace filename/content):\n' +
136
+ `CONTENT=$(echo -n 'YOUR_CONTENT' | base64) && ` +
137
+ `${curl} -s -X POST ${baseUrl}/v1/files/base64 ` +
138
+ `-H "${h}" ` +
139
+ '-H "Content-Type: application/json" ' +
140
+ `-d '{"filename":"report.md",` +
141
+ `"content_base64":"'"$CONTENT"'",` +
142
+ `"content_type":"text/markdown",` +
143
+ `"network":"${workspaceId}",` +
144
+ `"source":"openagents:${agentName}",` +
145
+ `"channel_name":"${channelName}"}'\n\n`
146
+ );
147
+ }
131
148
  }
132
149
 
150
+ const tmpDir = isWindows ? '$env:TEMP' : '/tmp';
133
151
  s += (
134
152
  '**List files:**\n' +
135
- `\`curl -s -H "${h}" ${baseUrl}/v1/files?network=${workspaceId}\`\n\n` +
136
- '**Download file:**\n' +
137
- `\`curl -s -H "${h}" ${baseUrl}/v1/files/{file_id}\`\n\n` +
153
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/files?network=${workspaceId}\`\n\n` +
154
+ '**Download file (text):**\n' +
155
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/files/{file_id}\`\n\n` +
156
+ '**Download file (binary/images) — save to disk, then use Read tool to view:**\n' +
157
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/files/{file_id} -o ${tmpDir}/{filename}\`\n\n` +
138
158
  '**File info (metadata):**\n' +
139
- `\`curl -s -H "${h}" ${baseUrl}/v1/files/{file_id}/info\`\n`
159
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/files/{file_id}/info\`\n`
140
160
  );
141
161
 
142
162
  if (!isPlan) {
143
163
  s += (
144
164
  '\n**Delete file:**\n' +
145
- `\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/files/{file_id}\`\n`
165
+ `\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/files/{file_id}\`\n`
146
166
  );
147
167
  }
148
168
 
@@ -157,48 +177,48 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
157
177
  s += (
158
178
  '**To browse a website**, exec these steps (use exec for each):\n' +
159
179
  `Step 1 — open tab: ` +
160
- `curl -s -X POST ${baseUrl}/v1/browser/tabs ` +
180
+ `${curl} -s -X POST ${baseUrl}/v1/browser/tabs ` +
161
181
  `-H "${h}" -H "Content-Type: application/json" ` +
162
182
  `-d '{"url":"https://example.com","network":"${workspaceId}",` +
163
183
  `"source":"openagents:${agentName}"}'\n` +
164
184
  `Step 2 — read content: ` +
165
- `curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID/snapshot\n` +
185
+ `${curl} -s -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID/snapshot\n` +
166
186
  `Step 3 — close tab: ` +
167
- `curl -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID\n` +
187
+ `${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID\n` +
168
188
  '(Replace TAB_ID with the id from step 1 response)\n\n'
169
189
  );
170
190
  }
171
191
 
172
192
  s += (
173
193
  '**List open tabs:**\n' +
174
- `\`curl -s -H "${h}" ${baseUrl}/v1/browser/tabs?network=${workspaceId}\`\n\n` +
194
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/browser/tabs?network=${workspaceId}\`\n\n` +
175
195
  '**Get page content (text):**\n' +
176
- `\`curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}/snapshot\`\n\n` +
196
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}/snapshot\`\n\n` +
177
197
  '**Get screenshot (PNG):**\n' +
178
- `\`curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}/screenshot\`\n`
198
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}/screenshot\`\n`
179
199
  );
180
200
 
181
201
  if (!isPlan) {
182
202
  s += (
183
203
  '\n**Open tab:**\n' +
184
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
204
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
185
205
  ` ${baseUrl}/v1/browser/tabs` +
186
206
  ` -d '{"url":"URL","network":"${workspaceId}",` +
187
207
  `"source":"openagents:${agentName}"}'\`\n\n` +
188
208
  '**Navigate:**\n' +
189
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
209
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
190
210
  ` ${baseUrl}/v1/browser/tabs/{tab_id}/navigate` +
191
211
  ` -d '{"url":"URL"}'\`\n\n` +
192
212
  '**Click element:**\n' +
193
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
213
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
194
214
  ` ${baseUrl}/v1/browser/tabs/{tab_id}/click` +
195
215
  ` -d '{"selector":"CSS_SELECTOR"}'\`\n\n` +
196
216
  '**Type text:**\n' +
197
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
217
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
198
218
  ` ${baseUrl}/v1/browser/tabs/{tab_id}/type` +
199
219
  ` -d '{"selector":"CSS_SELECTOR","text":"TEXT"}'\`\n\n` +
200
220
  '**Close tab:**\n' +
201
- `\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\`\n`
221
+ `\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\`\n`
202
222
  );
203
223
  }
204
224
 
@@ -209,9 +229,9 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
209
229
  sections.push(
210
230
  '\n### Message History\n\n' +
211
231
  '**Get recent messages in the current channel:**\n' +
212
- `\`curl -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=${channelName}&type=workspace.message&sort=desc&limit=20"\`\n\n` +
232
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=${channelName}&type=workspace.message&sort=desc&limit=20"\`\n\n` +
213
233
  '**Get messages from a specific channel:**\n' +
214
- `\`curl -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=CHANNEL_NAME&type=workspace.message&sort=desc&limit=20"\`\n`
234
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=CHANNEL_NAME&type=workspace.message&sort=desc&limit=20"\`\n`
215
235
  );
216
236
 
217
237
  // Post status update
@@ -219,7 +239,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
219
239
  sections.push(
220
240
  '\n### Post Status Update\n\n' +
221
241
  'Post a status/thinking message (visible in the workspace UI as an intermediate step):\n' +
222
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
242
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
223
243
  `${baseUrl}/v1/events -d '{"type":"workspace.message.posted",` +
224
244
  `"source":"openagents:${agentName}","target":"channel/${channelName}",` +
225
245
  `"payload":{"content":"YOUR_STATUS","message_type":"status"}}'\`\n`
@@ -234,14 +254,14 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
234
254
  'is replaced each time (send the full list with current statuses).\n\n' +
235
255
  '**Status values:** `pending`, `in_progress`, `completed`\n\n' +
236
256
  '**Update your to-do list:**\n' +
237
- `\`curl -s -X PUT -H "${h}" -H "Content-Type: application/json" ` +
257
+ `\`${curl} -s -X PUT -H "${h}" -H "Content-Type: application/json" ` +
238
258
  `${baseUrl}/v1/todos -d '{"todos":[` +
239
259
  `{"content":"First task","status":"in_progress"},` +
240
260
  `{"content":"Second task","status":"pending"}` +
241
261
  `],"network":"${workspaceId}","channel":"${channelName}",` +
242
262
  `"source":"openagents:${agentName}"}'\`\n\n` +
243
263
  '**Get your to-do list:**\n' +
244
- `\`curl -s -H "${h}" "${baseUrl}/v1/todos?network=${workspaceId}&channel=${channelName}"\`\n\n` +
264
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/todos?network=${workspaceId}&channel=${channelName}"\`\n\n` +
245
265
  '**IMPORTANT:** When you receive a task with multiple steps or a list of things to do, ' +
246
266
  'ALWAYS create a to-do list first before starting work. This lets the user see your ' +
247
267
  'progress in real time. Update statuses as you work through each item.\n' +
@@ -259,14 +279,14 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
259
279
  'Use cases: check back on a deploy, retry after a rate limit, remind ' +
260
280
  'yourself to follow up.\n\n' +
261
281
  '**Create a timer:**\n' +
262
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
282
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
263
283
  `${baseUrl}/v1/timers -d '{"delay":300,"message":"Check the build",` +
264
284
  `"network":"${workspaceId}","channel":"${channelName}",` +
265
285
  `"source":"openagents:${agentName}"}'\`\n\n` +
266
286
  '**List active timers:**\n' +
267
- `\`curl -s -H "${h}" "${baseUrl}/v1/timers?network=${workspaceId}&channel=${channelName}"\`\n\n` +
287
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/timers?network=${workspaceId}&channel=${channelName}"\`\n\n` +
268
288
  '**Cancel a timer:**\n' +
269
- `\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/timers/TIMER_ID\`\n`
289
+ `\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/timers/TIMER_ID\`\n`
270
290
  );
271
291
  }
272
292
 
@@ -280,21 +300,21 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
280
300
  '**Schedule:** Specify `hour` (0-23 UTC), `minute` (0-59), and optional ' +
281
301
  '`days` array (0=Mon, 6=Sun). Omit `days` for every day.\n\n' +
282
302
  '**Create a routine:**\n' +
283
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
303
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
284
304
  `${baseUrl}/v1/routines -d '{"name":"Daily PR Review","message":"Review open PRs",` +
285
305
  `"hour":8,"minute":0,` +
286
306
  `"network":"${workspaceId}","channel":"${channelName}",` +
287
307
  `"source":"openagents:${agentName}"}'\`\n\n` +
288
308
  '**Create a weekday-only routine (Mon-Fri):**\n' +
289
- `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
309
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
290
310
  `${baseUrl}/v1/routines -d '{"name":"Morning Standup","message":"Post standup summary",` +
291
311
  `"hour":9,"minute":0,"days":[0,1,2,3,4],` +
292
312
  `"network":"${workspaceId}","channel":"${channelName}",` +
293
313
  `"source":"openagents:${agentName}"}'\`\n\n` +
294
314
  '**List active routines:**\n' +
295
- `\`curl -s -H "${h}" "${baseUrl}/v1/routines?network=${workspaceId}&channel=${channelName}"\`\n\n` +
315
+ `\`${curl} -s -H "${h}" "${baseUrl}/v1/routines?network=${workspaceId}&channel=${channelName}"\`\n\n` +
296
316
  '**Cancel a routine:**\n' +
297
- `\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/routines/ROUTINE_ID\`\n`
317
+ `\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/routines/ROUTINE_ID\`\n`
298
318
  );
299
319
  }
300
320
 
@@ -302,12 +322,32 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
302
322
  sections.push(
303
323
  '\n### Discover Agents\n\n' +
304
324
  '**List all agents in the workspace (with status and role):**\n' +
305
- `\`curl -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId}\`\n`
325
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId}\`\n`
306
326
  );
307
327
 
308
328
  return sections.join('\n');
309
329
  }
310
330
 
331
+ /**
332
+ * Guardrails shared across all adapter prompt builders.
333
+ */
334
+ function buildGuardrails() {
335
+ return (
336
+ '\nIMPORTANT: Never use AskUserQuestion. ' +
337
+ 'AskUserQuestion blocks the subprocess and will hang the thread. ' +
338
+ 'If you need to ask the user something, just write the question ' +
339
+ 'as your text response.\n' +
340
+ '\nIMPORTANT: When the user gives you a numbered list, bulleted list, or ' +
341
+ 'multiple tasks in a single message, you MUST create a to-do list BEFORE ' +
342
+ 'doing any work. This is mandatory — no exceptions, even for simple tasks. ' +
343
+ 'The to-do list lets the user track your progress in real time.\n' +
344
+ '\nIMPORTANT: Do NOT use built-in scheduling tools (CronCreate, CronDelete, ' +
345
+ 'CronList, ScheduleWakeup). For timers, routines, and recurring tasks, ' +
346
+ 'ALWAYS use the workspace REST API (curl commands in your skill instructions). ' +
347
+ 'Built-in scheduling is local-only and won\'t appear in the workspace.\n'
348
+ );
349
+ }
350
+
311
351
  /**
312
352
  * Build the system prompt for Claude adapter (MCP-based).
313
353
  * Claude gets identity + collaboration instructions but NOT API skills.
@@ -323,6 +363,7 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
323
363
  'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n'
324
364
  );
325
365
  parts.push(buildCollaborationPrompt());
366
+ parts.push(buildA2UIPrompt());
326
367
 
327
368
  if (mode === 'plan') {
328
369
  parts.push(
@@ -331,28 +372,86 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
331
372
  );
332
373
  }
333
374
 
334
- parts.push(
335
- '\nIMPORTANT: Never use AskUserQuestion. ' +
336
- 'AskUserQuestion blocks the subprocess and will hang the thread. ' +
337
- 'If you need to ask the user something, just write the question ' +
338
- 'as your text response.\n'
339
- );
375
+ parts.push(buildGuardrails());
340
376
 
341
- parts.push(
342
- '\nIMPORTANT: When the user gives you a numbered list, bulleted list, or ' +
343
- 'multiple tasks in a single message, you MUST create a to-do list BEFORE ' +
344
- 'doing any work. This is mandatory — no exceptions, even for simple tasks. ' +
345
- 'The to-do list lets the user track your progress in real time.\n'
346
- );
377
+ return parts.join('\n');
378
+ }
347
379
 
348
- parts.push(
349
- '\nIMPORTANT: Do NOT use built-in scheduling tools (CronCreate, CronDelete, ' +
350
- 'CronList, ScheduleWakeup). For timers, routines, and recurring tasks, ' +
351
- 'ALWAYS use the workspace REST API (curl commands in your skill instructions). ' +
352
- 'Built-in scheduling is local-only and won\'t appear in the workspace.\n'
380
+ /**
381
+ * Teach the LLM how to emit interactive UI alongside its text response.
382
+ * The frontend (OpenAgents Go) renders any A2UI-shaped JSON spec inline
383
+ * in the chat bubble; the agent-connector strips the fenced block before
384
+ * posting and ferries the spec via payload.spec.
385
+ */
386
+ function buildA2UIPrompt() {
387
+ return (
388
+ '\n## Rendering interactive UI\n' +
389
+ 'When you want the user to interact with structured UI (a button choice, ' +
390
+ 'a form, a chart, a table, a confirmation dialog) instead of just reading ' +
391
+ 'text, emit a fenced code block tagged `a2ui` containing a JSON spec. The ' +
392
+ "frontend renders it inline below any markdown narration you write.\n\n" +
393
+ "The spec is a tree of `{ type, props, children?, action? }` nodes. " +
394
+ 'Supported component types include `Stack`, `Heading`, `Text`, `Image`, ' +
395
+ '`Icon`, `Button`, `ChoiceList`, `ConfirmDialog`, `AmountInput`, `Card`, ' +
396
+ '`Divider`, `Spacer`, `Alert`, `LineChart`, `PieChart`, `AssetPrice`, ' +
397
+ '`BalanceCard`, `TransactionList`, `TransactionRow`. Unknown types render ' +
398
+ 'as a placeholder chip, so feel free to compose freely.\n\n' +
399
+ 'Interactive components attach an `action` object with a `name` field you ' +
400
+ 'choose. Example:\n\n' +
401
+ '```a2ui\n' +
402
+ '{\n' +
403
+ ' "type": "Stack",\n' +
404
+ ' "props": { "direction": "vertical", "spacing": 12 },\n' +
405
+ ' "children": [\n' +
406
+ ' { "type": "Heading", "props": { "text": "Pick a time", "level": 2 } },\n' +
407
+ ' { "type": "Button", "props": { "label": "Morning", "action": { "name": "pick_morning" } } },\n' +
408
+ ' { "type": "Button", "props": { "label": "Evening", "action": { "name": "pick_evening" } } }\n' +
409
+ ' ]\n' +
410
+ '}\n' +
411
+ '```\n\n' +
412
+ '### Exact prop names (these are checked verbatim — guessing will render "No data")\n\n' +
413
+ 'Layout / content:\n' +
414
+ '- `Stack` — props: `direction` ("vertical"|"horizontal"), `spacing`, `alignment`\n' +
415
+ '- `Card` — props: `title`, `padding`, `cornerRadius`\n' +
416
+ '- `Divider`, `Spacer` — props: `size`, `orientation`\n' +
417
+ '- `Heading` — props: `text`, `level` (1–4)\n' +
418
+ '- `Text` — props: `content` **(NOT `text`)**, `style`, `weight`, `color`\n' +
419
+ '- `Image` — props: `url`, `name`, `contentMode`, `width`, `height`\n' +
420
+ '- `Icon` — props: `name`, `size`, `color`\n\n' +
421
+ 'Interactive:\n' +
422
+ '- `Button` — props: `label`, `style` ("primary"|"secondary"|"destructive"), `icon`, `disabled`, `action: {name, params?}`\n' +
423
+ '- `ConfirmDialog` — props: `title`, `message`, `confirmLabel`, `cancelLabel`, `triggerLabel`, `action`\n' +
424
+ '- `ChoiceList` — props: `question`, `options: [{id, label}]`, `action`\n' +
425
+ '- `AmountInput` — props: `label`, `placeholder`, `currency`, `action`\n' +
426
+ '- `input` (lowercase) — props: `inputType` ("text"|"choice"|"number"|"date"), `id`, `label`, `placeholder`, `options`\n\n' +
427
+ 'Feedback:\n' +
428
+ '- `Alert` — props: `title`, `message`, `severity` ("info"|"success"|"warning"|"error"), `dismissible`, `action`\n\n' +
429
+ 'Charts (Swift Charts under the hood — needs **non-empty** data with the right key):\n' +
430
+ '- `LineChart` — props: `title`, `color`, `points: [{x, y}]` **(NOT `data`)**\n' +
431
+ '- `PieChart` — props: `title`, `segments: [{label, value, color?}]` **(NOT `slices`)**, `showLegend`\n' +
432
+ '- `chart` (lowercase) — simple sparkline/bar: props: `style` ("sparkline"|"bar"), `data: [number, ...]`, `labels`, `color`\n\n' +
433
+ 'Data display:\n' +
434
+ '- `metric` (lowercase) — props: `label`, `value`, `caption`, `captionColor`, `icon`\n' +
435
+ '- `list` (lowercase) — props: `items: [{title, subtitle, trailing, icon}]`, `maxVisible`, `expandLabel`\n' +
436
+ '- `table` (lowercase) — **key-value rows only, not multi-column**: props: `rows: [{label, value}]`, `maxVisible`\n\n' +
437
+ 'There is no multi-column data table in the catalog. For tabular data with ' +
438
+ 'columns you have two choices: (a) use `list` with title/subtitle/trailing ' +
439
+ 'mapped to your columns, or (b) compose a Stack of horizontal Stacks of Text ' +
440
+ 'manually. Pick whichever reads better.\n\n' +
441
+ 'If your data is empty or you can\'t fit it into the available primitives, ' +
442
+ 'fall back to plain markdown — don\'t emit a spec just to wrap text in a card.\n\n' +
443
+ 'When the user interacts with a rendered component, you will receive a ' +
444
+ "user message shaped like `[ui_action] action=pick_morning tool_call_id=... value=...`. " +
445
+ "Use the `action` name (which you chose) to decide what to do next; the " +
446
+ "spec you emitted earlier is in your conversation history for context.\n\n" +
447
+ 'Use a spec when:\n' +
448
+ '- The user needs to make a discrete choice (offer buttons rather than ' +
449
+ 'asking them to type a free-form answer).\n' +
450
+ '- Structured data is easier to scan as a chart or table than as prose.\n' +
451
+ '- You need explicit confirmation before a destructive action.\n' +
452
+ "- A form would gather several fields faster than back-and-forth chat.\n\n" +
453
+ "Don't emit a spec for ordinary prose answers — narration alone is fine.\n"
353
454
  );
354
-
355
- return parts.join('\n');
356
455
  }
357
456
 
358
457
  /**
@@ -362,10 +461,12 @@ function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoi
362
461
  const parts = [];
363
462
  parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
364
463
  parts.push(buildCollaborationPrompt());
464
+ parts.push(buildA2UIPrompt());
365
465
  parts.push(buildModePrompt(mode));
366
466
  parts.push(buildApiSkillsPrompt({
367
467
  endpoint, workspaceId, token, agentName, channelName, disabledModules, mode,
368
468
  }));
469
+ parts.push(buildGuardrails());
369
470
  return parts.join('\n');
370
471
  }
371
472
 
@@ -394,7 +495,7 @@ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channel
394
495
  '---\n\n'
395
496
  );
396
497
 
397
- return frontmatter + identity + '\n' + collab + '\n' + body;
498
+ return frontmatter + identity + '\n' + collab + '\n' + body + '\n' + buildGuardrails();
398
499
  }
399
500
 
400
501
  /**
@@ -405,7 +506,7 @@ function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoi
405
506
  const collab = buildCollaborationPrompt();
406
507
  const modePrompt = buildModePrompt(mode);
407
508
  const api = buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode });
408
- return identity + '\n' + collab + '\n' + modePrompt + '\n' + api;
509
+ return identity + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
409
510
  }
410
511
 
411
512
  /**
@@ -429,7 +530,7 @@ function buildOpenCodeSkillMd({ endpoint, workspaceId, token, agentName, channel
429
530
  `You are agent '${agentName}' connected to OpenAgents workspace ${workspaceId}.\n` +
430
531
  'Use these APIs via bash + curl to interact with the workspace.\n\n';
431
532
 
432
- return frontmatter + identity + api;
533
+ return frontmatter + identity + api + '\n' + buildGuardrails();
433
534
  }
434
535
 
435
536
  /**
@@ -460,13 +561,44 @@ function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelNa
460
561
  ' or collaborating with other agents via @mentions.\n' +
461
562
  '---\n\n';
462
563
 
463
- return frontmatter + identity + '\n' + collab + '\n' + api;
564
+ return frontmatter + identity + '\n' + collab + '\n' + api + '\n' + buildGuardrails();
565
+ }
566
+
567
+ /**
568
+ * Build a SKILL.md file for Cursor CLI's skill auto-discovery.
569
+ *
570
+ * Written to .cursor/skills/openagents-workspace.md before each CLI spawn.
571
+ * Cursor discovers skills from the .cursor/skills/ directory automatically.
572
+ */
573
+ function buildCursorSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
574
+ const api = buildApiSkillsPrompt({
575
+ endpoint, workspaceId, token, agentName,
576
+ channelName: channelName || 'general',
577
+ disabledModules,
578
+ mode: 'execute',
579
+ });
580
+
581
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
582
+ const collab = buildCollaborationPrompt();
583
+
584
+ const frontmatter =
585
+ '---\n' +
586
+ 'name: openagents-workspace\n' +
587
+ 'description: |\n' +
588
+ ' OpenAgents Workspace collaboration tools — shared files, browser,\n' +
589
+ ' and multi-agent coordination. Use when: sharing files or reports,\n' +
590
+ ' browsing websites, reading shared files, checking workspace agents,\n' +
591
+ ' or collaborating with other agents via @mentions.\n' +
592
+ '---\n\n';
593
+
594
+ return frontmatter + identity + '\n' + collab + '\n' + api + '\n' + buildGuardrails();
464
595
  }
465
596
 
466
597
  module.exports = {
467
598
  buildWorkspaceIdentity,
468
599
  buildCollaborationPrompt,
469
600
  buildModePrompt,
601
+ buildGuardrails,
470
602
  buildApiSkillsPrompt,
471
603
  buildClaudeSystemPrompt,
472
604
  buildOpenclawSystemPrompt,
@@ -474,4 +606,5 @@ module.exports = {
474
606
  buildOpenCodeSystemPrompt,
475
607
  buildOpenCodeSkillMd,
476
608
  buildClaudeSkillMd,
609
+ buildCursorSkillMd,
477
610
  };
package/src/mcp-server.js CHANGED
@@ -451,7 +451,14 @@ class McpServer {
451
451
  if (mt === 'status') return null;
452
452
  const sender = m.senderName || m.senderType || '';
453
453
  const content = m.content || '';
454
- return `[${sender}] ${content}`;
454
+ let line = `[${sender}] ${content}`;
455
+ if (m.attachments && m.attachments.length > 0) {
456
+ const atts = m.attachments.map((a) =>
457
+ `[Attached: ${a.filename || 'file'} (file_id: ${a.fileId || '?'})]`
458
+ ).join(' ');
459
+ line += `\n ${atts}`;
460
+ }
461
+ return line;
455
462
  }).filter(Boolean);
456
463
  if (!lines.length) return text('No messages yet.');
457
464
  return text(lines.join('\n'));
@@ -156,12 +156,15 @@ class WorkspaceClient {
156
156
  */
157
157
  async sendMessage(workspaceId, channelName, token, content, {
158
158
  senderType = 'agent', senderName, messageType = 'chat', metadata, attachments, sessionId,
159
+ spec, specToolCallId,
159
160
  } = {}) {
160
161
  const sourcePrefix = senderType === 'agent' ? 'openagents' : 'human';
161
162
  const source = senderName ? `${sourcePrefix}:${senderName}` : `${sourcePrefix}:unknown`;
162
163
 
163
164
  const payload = { content, message_type: messageType };
164
165
  if (attachments && attachments.length) payload.attachments = attachments;
166
+ if (spec) payload.spec = spec;
167
+ if (specToolCallId) payload.spec_tool_call_id = specToolCallId;
165
168
 
166
169
  return this.sendEvent(workspaceId, {
167
170
  type: 'workspace.message.posted',
@@ -172,6 +175,30 @@ class WorkspaceClient {
172
175
  }, token, sessionId);
173
176
  }
174
177
 
178
+ /**
179
+ * Poll workspace.tool_result events — the client's response to an agent's
180
+ * render_ui invocation. Returns events directly (no message normalization)
181
+ * because the caller treats them differently than chat messages.
182
+ */
183
+ async pollToolResults(workspaceId, token, { after, limit = 50 } = {}) {
184
+ const params = new URLSearchParams({
185
+ network: workspaceId,
186
+ type: 'workspace.tool_result',
187
+ limit: String(limit),
188
+ });
189
+ if (after) params.set('after', after);
190
+
191
+ const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
192
+ const result = data.data || data;
193
+ const events = (result && result.events) || [];
194
+
195
+ let cursor = null;
196
+ if (events.length > 0) {
197
+ cursor = events[events.length - 1].id || null;
198
+ }
199
+ return { events, cursor };
200
+ }
201
+
175
202
  /**
176
203
  * Poll messages in a channel via GET /v1/events.
177
204
  * @returns {Array} message-compatible objects