agent-relay 8.7.1 → 8.8.0

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.
Files changed (51) hide show
  1. package/dist/auto/classifier.d.ts +29 -0
  2. package/dist/auto/classifier.d.ts.map +1 -0
  3. package/dist/auto/classifier.js +126 -0
  4. package/dist/auto/classifier.js.map +1 -0
  5. package/dist/auto/composer.d.ts +105 -0
  6. package/dist/auto/composer.d.ts.map +1 -0
  7. package/dist/auto/composer.js +439 -0
  8. package/dist/auto/composer.js.map +1 -0
  9. package/dist/auto/director-prompt.d.ts +25 -0
  10. package/dist/auto/director-prompt.d.ts.map +1 -0
  11. package/dist/auto/director-prompt.js +53 -0
  12. package/dist/auto/director-prompt.js.map +1 -0
  13. package/dist/auto/index.d.ts +15 -0
  14. package/dist/auto/index.d.ts.map +1 -0
  15. package/dist/auto/index.js +13 -0
  16. package/dist/auto/index.js.map +1 -0
  17. package/dist/cli/agent-relay-mcp.d.ts +1 -1
  18. package/dist/cli/agent-relay-mcp.d.ts.map +1 -1
  19. package/dist/cli/agent-relay-mcp.js +428 -518
  20. package/dist/cli/agent-relay-mcp.js.map +1 -1
  21. package/dist/cli/bootstrap.d.ts.map +1 -1
  22. package/dist/cli/bootstrap.js +2 -0
  23. package/dist/cli/bootstrap.js.map +1 -1
  24. package/dist/cli/commands/cloud-worker.d.ts +19 -0
  25. package/dist/cli/commands/cloud-worker.d.ts.map +1 -0
  26. package/dist/cli/commands/cloud-worker.js +376 -0
  27. package/dist/cli/commands/cloud-worker.js.map +1 -0
  28. package/dist/cli/commands/cloud.d.ts.map +1 -1
  29. package/dist/cli/commands/cloud.js +2 -0
  30. package/dist/cli/commands/cloud.js.map +1 -1
  31. package/dist/cli/commands/core.d.ts +1 -0
  32. package/dist/cli/commands/core.d.ts.map +1 -1
  33. package/dist/cli/commands/core.js +1 -1
  34. package/dist/cli/commands/core.js.map +1 -1
  35. package/dist/cli/commands/fleet.d.ts +16 -0
  36. package/dist/cli/commands/fleet.d.ts.map +1 -0
  37. package/dist/cli/commands/fleet.js +188 -0
  38. package/dist/cli/commands/fleet.js.map +1 -0
  39. package/dist/cli/commands/local-agent.d.ts.map +1 -1
  40. package/dist/cli/commands/local-agent.js +43 -11
  41. package/dist/cli/commands/local-agent.js.map +1 -1
  42. package/dist/cli/lib/broker-lifecycle.d.ts +5 -1
  43. package/dist/cli/lib/broker-lifecycle.d.ts.map +1 -1
  44. package/dist/cli/lib/broker-lifecycle.js +32 -2
  45. package/dist/cli/lib/broker-lifecycle.js.map +1 -1
  46. package/dist/cli/lib/fleet-sidecar.d.ts +53 -0
  47. package/dist/cli/lib/fleet-sidecar.d.ts.map +1 -0
  48. package/dist/cli/lib/fleet-sidecar.js +400 -0
  49. package/dist/cli/lib/fleet-sidecar.js.map +1 -0
  50. package/dist/index.cjs +2545 -16773
  51. package/package.json +10 -7
@@ -7,10 +7,10 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
7
  import { ListToolsRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { RelayCast, SDK_VERSION, WsClient } from '@relaycast/sdk';
9
9
  import { INVALID_AGENT_TOKEN_CODE, agentTokenRecoveryMessage, isInvalidAgentTokenError, isInvalidAgentTokenToolResult, } from '@agent-relay/sdk';
10
+ import { AgentRelay } from '@agent-relay/sdk';
10
11
  import { z } from 'zod';
11
- import { initTelemetry, shutdown as shutdownTelemetry, track, } from './telemetry/index.js';
12
+ import { initTelemetry, shutdown as shutdownTelemetry } from './telemetry/index.js';
12
13
  import { relaycastWorkspaceTelemetryOptions, withRelaycastTelemetry } from './lib/relaycast-telemetry.js';
13
- import { errorClassName } from './lib/telemetry-helpers.js';
14
14
  const DEFAULT_BASE_URL = 'https://gateway.relaycast.dev';
15
15
  export const AGENT_RELAY_MCP_VERSION = process.env.AGENT_RELAY_CLI_VERSION ?? SDK_VERSION ?? 'unknown';
16
16
  let mcpTelemetryExitHookInstalled = false;
@@ -30,6 +30,10 @@ const DEFAULT_SYSTEM_PROMPT = `You are an AI agent in a collaborative workspace
30
30
  - Reply to threads with "reply_to_thread"
31
31
  - React to messages with "add_reaction"
32
32
 
33
+ ## Fleet
34
+ - Use "query_nodes" to find fleet nodes by capability or name
35
+ - Use "spawn" to invoke the fleet spawn action on an eligible node
36
+
33
37
  ## Best Practices
34
38
  - Check your inbox regularly for new messages and mentions
35
39
  - Use channels for topic-based discussions
@@ -207,319 +211,6 @@ function createInitialSession(options) {
207
211
  wsInitAttempted: false,
208
212
  };
209
213
  }
210
- async function createWorkspace(name, baseUrl) {
211
- return (await RelayCast.createWorkspace(name, {
212
- baseUrl,
213
- ...relaycastWorkspaceTelemetryOptions(),
214
- }));
215
- }
216
- function extractWorkspaceKey(payload) {
217
- const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
218
- const value = payload.workspaceKey ??
219
- payload.workspace_key ??
220
- payload.apiKey ??
221
- payload.api_key ??
222
- data.workspaceKey ??
223
- data.workspace_key ??
224
- data.apiKey ??
225
- data.api_key;
226
- return typeof value === 'string' && value.trim() ? value : undefined;
227
- }
228
- function extractWorkspaceName(payload, fallback) {
229
- const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
230
- const value = payload.workspaceName ?? payload.workspace_name ?? payload.name ?? data.workspaceName;
231
- return typeof value === 'string' && value.trim() ? value : fallback;
232
- }
233
- function requireWorkspaceKey(session) {
234
- if (session.workspaceKey) {
235
- return;
236
- }
237
- throw new Error('Workspace key not configured. Call "create_workspace" first, or "set_workspace_key" if someone shared a workspace key.');
238
- }
239
- function jsonContent(value) {
240
- const structuredContent = typeof value === 'object' && value !== null && !Array.isArray(value)
241
- ? value
242
- : { value };
243
- return {
244
- content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
245
- structuredContent,
246
- };
247
- }
248
- function textContent(message, structuredContent = { message }) {
249
- return {
250
- content: [{ type: 'text', text: message }],
251
- structuredContent,
252
- };
253
- }
254
- function isSchemaObject(schema) {
255
- return Boolean(schema &&
256
- typeof schema === 'object' &&
257
- !Array.isArray(schema) &&
258
- typeof schema.safeParse !== 'function');
259
- }
260
- function getSchemaDescription(schema) {
261
- return isSchemaObject(schema) && typeof schema.description === 'string' ? schema.description : undefined;
262
- }
263
- function zodFromJsonSchema(schema) {
264
- if (schema === false) {
265
- return z.never();
266
- }
267
- if (!isSchemaObject(schema)) {
268
- return z.unknown();
269
- }
270
- let zodType;
271
- const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
272
- switch (schemaType) {
273
- case 'array':
274
- zodType = z.array(zodFromJsonSchema(schema.items));
275
- break;
276
- case 'boolean':
277
- zodType = z.boolean();
278
- break;
279
- case 'integer':
280
- zodType = z.number().int();
281
- break;
282
- case 'number':
283
- zodType = z.number();
284
- break;
285
- case 'object':
286
- if (schema.properties) {
287
- const required = new Set(schema.required ?? []);
288
- const shape = {};
289
- for (const [key, childSchema] of Object.entries(schema.properties)) {
290
- const child = zodFromJsonSchema(childSchema);
291
- shape[key] = required.has(key) ? child : child.optional();
292
- }
293
- zodType = z.object(shape).passthrough();
294
- }
295
- else {
296
- zodType = z.record(z.string(), z.unknown());
297
- }
298
- break;
299
- case 'string':
300
- zodType = z.string();
301
- break;
302
- default:
303
- zodType = z.unknown();
304
- break;
305
- }
306
- const description = getSchemaDescription(schema);
307
- return description ? zodType.describe(description) : zodType;
308
- }
309
- function actionToolInputSchema(schema) {
310
- const zodShape = zodObjectShape(schema);
311
- if (zodShape) {
312
- return zodShape;
313
- }
314
- if (!isSchemaObject(schema) || schema.type !== 'object') {
315
- return {
316
- input: z.unknown().describe('Action input payload. The action registry performs final validation.'),
317
- };
318
- }
319
- const required = new Set(schema.required ?? []);
320
- const shape = {};
321
- for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
322
- const child = zodFromJsonSchema(childSchema);
323
- shape[key] = required.has(key) ? child : child.optional();
324
- }
325
- return shape;
326
- }
327
- function actionInvocationInput(descriptor, args) {
328
- const schema = descriptor.inputSchema;
329
- if (zodObjectShape(schema)) {
330
- return args;
331
- }
332
- if (!isSchemaObject(schema) || schema.type !== 'object') {
333
- return typeof args === 'object' && args !== null && 'input' in args
334
- ? args.input
335
- : args;
336
- }
337
- return args;
338
- }
339
- function zodObjectShape(schema) {
340
- if (schema instanceof z.ZodObject) {
341
- return schema.shape;
342
- }
343
- return undefined;
344
- }
345
- function serializableActionDescriptor(descriptor) {
346
- return {
347
- name: descriptor.name,
348
- description: descriptor.description,
349
- visibility: descriptor.visibility,
350
- ...(descriptor.inputSchema ? { inputSchema: serializableActionSchema(descriptor.inputSchema) } : {}),
351
- ...(descriptor.outputSchema ? { outputSchema: serializableActionSchema(descriptor.outputSchema) } : {}),
352
- };
353
- }
354
- function serializableActionSchema(schema) {
355
- if (isSchemaObject(schema)) {
356
- return schema;
357
- }
358
- if (isZodLikeSchema(schema)) {
359
- return {
360
- type: 'zod',
361
- ...(schema.description ? { description: schema.description } : {}),
362
- };
363
- }
364
- return schema;
365
- }
366
- function isZodLikeSchema(schema) {
367
- return Boolean(schema &&
368
- typeof schema === 'object' &&
369
- !Array.isArray(schema) &&
370
- typeof schema.safeParse === 'function');
371
- }
372
- function registerAgentRelayActionTools(server, actions, getSession, onAuditEvent, getAgentClient, actionToolNames) {
373
- if (!actions) {
374
- return;
375
- }
376
- /**
377
- * Fire-and-forget invocation through the relay: returns an immediate ack
378
- * (with an `invocation_id`) and does NOT run the handler inline. Falls back to
379
- * the local in-process registry when the relay action surface is unavailable.
380
- */
381
- const invokeAction = async (name, input) => {
382
- const relayActions = getRelayAgentActions(getAgentClient);
383
- if (relayActions) {
384
- try {
385
- const ack = await relayActions.invoke(name, asInputRecord(input));
386
- return jsonContent({ ok: true, status: 'invoked', invocation: ack });
387
- }
388
- catch (error) {
389
- return { ...jsonContent({ ok: false, error: errorMessage(error) }), isError: true };
390
- }
391
- }
392
- const session = getSession();
393
- const result = await actions.invoke({
394
- name,
395
- input,
396
- context: {
397
- caller: { name: session.agentName ?? 'mcp', type: 'agent' },
398
- emit: onAuditEvent,
399
- },
400
- });
401
- return result.ok ? jsonContent(result) : { ...jsonContent(result), isError: true };
402
- };
403
- server.registerTool('list_actions', {
404
- title: 'List Actions',
405
- description: 'List Agent Relay actions available to this agent.',
406
- inputSchema: {},
407
- outputSchema: jsonResult,
408
- annotations: {
409
- readOnlyHint: true,
410
- destructiveHint: false,
411
- idempotentHint: true,
412
- openWorldHint: false,
413
- },
414
- }, async () => jsonContent({
415
- actions: (await actions.list({ visibility: 'agent' })).map(serializableActionDescriptor),
416
- }));
417
- server.registerTool('invoke_action', {
418
- title: 'Invoke Action',
419
- description: 'Invoke a registered Agent Relay action by name. Fire-and-forget: returns an ack with an invocation id; the result arrives asynchronously to the action handler.',
420
- inputSchema: {
421
- name: z.string().describe('Registered action name'),
422
- input: z.unknown().describe('Action input payload'),
423
- },
424
- outputSchema: jsonResult,
425
- annotations: {
426
- readOnlyHint: false,
427
- destructiveHint: false,
428
- idempotentHint: false,
429
- openWorldHint: false,
430
- },
431
- }, async ({ name, input }) => invokeAction(name, input));
432
- void actions
433
- .list({ visibility: 'agent' })
434
- .then((descriptors) => {
435
- for (const descriptor of descriptors) {
436
- actionToolNames?.add(descriptor.name);
437
- server.registerTool(descriptor.name, {
438
- title: descriptor.name,
439
- description: descriptor.description,
440
- inputSchema: actionToolInputSchema(descriptor.inputSchema),
441
- outputSchema: jsonResult,
442
- annotations: {
443
- readOnlyHint: false,
444
- destructiveHint: false,
445
- idempotentHint: false,
446
- openWorldHint: false,
447
- },
448
- }, async (args) => invokeAction(descriptor.name, actionInvocationInput(descriptor, args)));
449
- }
450
- })
451
- .catch(() => undefined);
452
- }
453
- /** The relay-backed action surface on the live agent client, when available. */
454
- function getRelayAgentActions(getAgentClient) {
455
- if (!getAgentClient) {
456
- return undefined;
457
- }
458
- try {
459
- return getAgentClient().actions;
460
- }
461
- catch {
462
- return undefined;
463
- }
464
- }
465
- function asInputRecord(input) {
466
- if (input === undefined || input === null) {
467
- return undefined;
468
- }
469
- if (typeof input === 'object' && !Array.isArray(input)) {
470
- return input;
471
- }
472
- return { input };
473
- }
474
- function errorMessage(error) {
475
- return error instanceof Error ? error.message : String(error);
476
- }
477
- function createRegisteredAgent(agentName, agentToken) {
478
- return { agentName, agentToken };
479
- }
480
- export async function registerAgentWithRebind({ session, setSession, getRelay, name, type, persona, metadata, strictAgentName, preferredAgentName, forcedAgentType, }) {
481
- requireWorkspaceKey(session);
482
- const configuredName = session.agentName ?? preferredAgentName?.trim() ?? null;
483
- const warnings = [];
484
- const effectiveName = strictAgentName && configuredName ? configuredName : name;
485
- if (strictAgentName && configuredName && name.trim() !== configuredName) {
486
- warnings.push(`Strict worker identity is enabled; ignoring requested name "${name}" and using "${configuredName}".`);
487
- }
488
- const effectiveType = forcedAgentType ?? type;
489
- if (forcedAgentType && type && type !== forcedAgentType) {
490
- warnings.push(`Forced worker type is enabled; ignoring requested type "${type}" and using "${forcedAgentType}".`);
491
- }
492
- if (session.agentToken && effectiveName && strictAgentName) {
493
- // If the session tracks per-identity agents, only short-circuit when the
494
- // strict-named identity is still registered. After an `agent_token_invalid`
495
- // recovery the entry is dropped from the map, which lets this fall through
496
- // to a fresh registerOrRotate instead of handing back the dead token.
497
- const cachedAgent = session.agents?.get(effectiveName);
498
- const knowsIdentities = session.agents !== undefined;
499
- if (!knowsIdentities || cachedAgent) {
500
- return {
501
- name: effectiveName,
502
- token: cachedAgent?.agentToken ?? session.agentToken,
503
- registered_name: effectiveName,
504
- warnings,
505
- };
506
- }
507
- }
508
- const relay = getRelay();
509
- const result = await relay.agents.registerOrRotate({
510
- name: effectiveName,
511
- type: effectiveType,
512
- persona,
513
- metadata,
514
- });
515
- const reboundName = result.name?.trim() ? result.name : effectiveName;
516
- setSession({ agentToken: result.token, agentName: reboundName });
517
- return {
518
- ...result,
519
- registered_name: reboundName,
520
- warnings,
521
- };
522
- }
523
214
  class SubscriptionManager {
524
215
  subscriptions = new Set();
525
216
  subscribe(uri) {
@@ -574,15 +265,11 @@ function eventToResourceUris(event) {
574
265
  case 'member.joined':
575
266
  case 'member.left':
576
267
  return ['relay://channels'];
577
- case 'webhook.received': {
268
+ case 'webhook.received':
269
+ case 'command.invoked': {
578
270
  const channel = getStringEventField(event, 'channel');
579
271
  return channel ? [`relay://channels/${channel}/messages`] : [];
580
272
  }
581
- case 'action.invoked':
582
- case 'action.completed':
583
- case 'action.failed':
584
- // Actions are not channel-scoped; surface invocations via the inbox.
585
- return ['relay://inbox'];
586
273
  case 'reaction.added':
587
274
  case 'reaction.removed':
588
275
  return ['relay://inbox'];
@@ -701,208 +388,383 @@ function formatInbox(inbox, selfName) {
701
388
  lines.push(` :${reaction.emoji}: on your message in #${reaction.channelName} by @${reaction.agentName}`);
702
389
  }
703
390
  }
704
- return lines.length === 1 ? '' : lines.join('\n');
391
+ return lines.length === 1 ? '' : lines.join('\n');
392
+ }
393
+ function readAsIdentity(args) {
394
+ const [input] = args;
395
+ if (typeof input !== 'object' || input === null)
396
+ return undefined;
397
+ const as = input.as;
398
+ return typeof as === 'string' ? as : undefined;
399
+ }
400
+ function invalidAgentTokenToolResult() {
401
+ const text = agentTokenRecoveryMessage();
402
+ return {
403
+ content: [{ type: 'text', text }],
404
+ structuredContent: {
405
+ error: { code: INVALID_AGENT_TOKEN_CODE, message: text },
406
+ },
407
+ isError: true,
408
+ };
409
+ }
410
+ function enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken) {
411
+ const original = mcpServer.registerTool.bind(mcpServer);
412
+ const mutableServer = mcpServer;
413
+ mutableServer.registerTool = (name, config, handler) => {
414
+ if (!handler) {
415
+ return original(name, config, handler);
416
+ }
417
+ const wrapped = async (...args) => {
418
+ const asIdentity = readAsIdentity(args);
419
+ let result;
420
+ try {
421
+ result = await handler(...args);
422
+ }
423
+ catch (err) {
424
+ if (name !== 'register_agent' && isInvalidAgentTokenError(err)) {
425
+ invalidateAgentToken(asIdentity);
426
+ return invalidAgentTokenToolResult();
427
+ }
428
+ throw err;
429
+ }
430
+ if (name !== 'register_agent' && isInvalidAgentTokenToolResult(result)) {
431
+ invalidateAgentToken(asIdentity);
432
+ if (hasContentArray(result)) {
433
+ result.content.push({ type: 'text', text: agentTokenRecoveryMessage() });
434
+ }
435
+ return result;
436
+ }
437
+ if (SKIP_PIGGYBACK.has(name) || !getSession().agentToken || !hasContentArray(result)) {
438
+ return result;
439
+ }
440
+ try {
441
+ const inbox = await getAgentClient(asIdentity).inbox();
442
+ const inboxText = formatInbox(inbox, asIdentity ?? getSession().agentName);
443
+ if (inboxText) {
444
+ result.content.push({ type: 'text', text: inboxText });
445
+ }
446
+ }
447
+ catch (err) {
448
+ if (isInvalidAgentTokenError(err)) {
449
+ invalidateAgentToken(asIdentity);
450
+ }
451
+ }
452
+ return result;
453
+ };
454
+ return original(name, config, wrapped);
455
+ };
456
+ }
457
+ async function createWorkspace(name, baseUrl) {
458
+ return (await RelayCast.createWorkspace(name, {
459
+ baseUrl,
460
+ ...relaycastWorkspaceTelemetryOptions(),
461
+ }));
462
+ }
463
+ function extractWorkspaceKey(payload) {
464
+ const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
465
+ const value = payload.workspaceKey ??
466
+ payload.workspace_key ??
467
+ payload.apiKey ??
468
+ payload.api_key ??
469
+ data.workspaceKey ??
470
+ data.workspace_key ??
471
+ data.apiKey ??
472
+ data.api_key;
473
+ return typeof value === 'string' && value.trim() ? value : undefined;
474
+ }
475
+ function extractWorkspaceName(payload, fallback) {
476
+ const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
477
+ const value = payload.workspaceName ?? payload.workspace_name ?? payload.name ?? data.workspaceName;
478
+ return typeof value === 'string' && value.trim() ? value : fallback;
479
+ }
480
+ function requireWorkspaceKey(session) {
481
+ if (session.workspaceKey) {
482
+ return;
483
+ }
484
+ throw new Error('Workspace key not configured. Call "create_workspace" first, or "set_workspace_key" if someone shared a workspace key.');
485
+ }
486
+ function jsonContent(value) {
487
+ const structuredContent = typeof value === 'object' && value !== null && !Array.isArray(value)
488
+ ? value
489
+ : { value };
490
+ return {
491
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
492
+ structuredContent,
493
+ };
494
+ }
495
+ function textContent(message, structuredContent = { message }) {
496
+ return {
497
+ content: [{ type: 'text', text: message }],
498
+ structuredContent,
499
+ };
500
+ }
501
+ function isSchemaObject(schema) {
502
+ return Boolean(schema &&
503
+ typeof schema === 'object' &&
504
+ !Array.isArray(schema) &&
505
+ typeof schema.safeParse !== 'function');
506
+ }
507
+ function getSchemaDescription(schema) {
508
+ return isSchemaObject(schema) && typeof schema.description === 'string' ? schema.description : undefined;
509
+ }
510
+ function zodFromJsonSchema(schema) {
511
+ if (schema === false) {
512
+ return z.never();
513
+ }
514
+ if (!isSchemaObject(schema)) {
515
+ return z.unknown();
516
+ }
517
+ let zodType;
518
+ const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
519
+ switch (schemaType) {
520
+ case 'array':
521
+ zodType = z.array(zodFromJsonSchema(schema.items));
522
+ break;
523
+ case 'boolean':
524
+ zodType = z.boolean();
525
+ break;
526
+ case 'integer':
527
+ zodType = z.number().int();
528
+ break;
529
+ case 'number':
530
+ zodType = z.number();
531
+ break;
532
+ case 'object':
533
+ if (schema.properties) {
534
+ const required = new Set(schema.required ?? []);
535
+ const shape = {};
536
+ for (const [key, childSchema] of Object.entries(schema.properties)) {
537
+ const child = zodFromJsonSchema(childSchema);
538
+ shape[key] = required.has(key) ? child : child.optional();
539
+ }
540
+ zodType = z.object(shape).passthrough();
541
+ }
542
+ else {
543
+ zodType = z.record(z.string(), z.unknown());
544
+ }
545
+ break;
546
+ case 'string':
547
+ zodType = z.string();
548
+ break;
549
+ default:
550
+ zodType = z.unknown();
551
+ break;
552
+ }
553
+ const description = getSchemaDescription(schema);
554
+ return description ? zodType.describe(description) : zodType;
555
+ }
556
+ function actionToolInputSchema(schema) {
557
+ const zodShape = zodObjectShape(schema);
558
+ if (zodShape) {
559
+ return zodShape;
560
+ }
561
+ if (!isSchemaObject(schema) || schema.type !== 'object') {
562
+ return {
563
+ input: z.unknown().describe('Action input payload. The action registry performs final validation.'),
564
+ };
565
+ }
566
+ const required = new Set(schema.required ?? []);
567
+ const shape = {};
568
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
569
+ const child = zodFromJsonSchema(childSchema);
570
+ shape[key] = required.has(key) ? child : child.optional();
571
+ }
572
+ return shape;
573
+ }
574
+ function actionInvocationInput(descriptor, args) {
575
+ const schema = descriptor.inputSchema;
576
+ if (zodObjectShape(schema)) {
577
+ return args;
578
+ }
579
+ if (!isSchemaObject(schema) || schema.type !== 'object') {
580
+ return typeof args === 'object' && args !== null && 'input' in args
581
+ ? args.input
582
+ : args;
583
+ }
584
+ return args;
705
585
  }
706
- function readAsIdentity(args) {
707
- const [input] = args;
708
- if (typeof input !== 'object' || input === null)
709
- return undefined;
710
- const as = input.as;
711
- return typeof as === 'string' ? as : undefined;
586
+ function zodObjectShape(schema) {
587
+ if (schema instanceof z.ZodObject) {
588
+ return schema.shape;
589
+ }
590
+ return undefined;
712
591
  }
713
- function invalidAgentTokenToolResult() {
714
- const text = agentTokenRecoveryMessage();
592
+ function serializableActionDescriptor(descriptor) {
715
593
  return {
716
- content: [{ type: 'text', text }],
717
- structuredContent: {
718
- error: { code: INVALID_AGENT_TOKEN_CODE, message: text },
719
- },
720
- isError: true,
594
+ name: descriptor.name,
595
+ description: descriptor.description,
596
+ visibility: descriptor.visibility,
597
+ ...(descriptor.inputSchema ? { inputSchema: serializableActionSchema(descriptor.inputSchema) } : {}),
598
+ ...(descriptor.outputSchema ? { outputSchema: serializableActionSchema(descriptor.outputSchema) } : {}),
721
599
  };
722
600
  }
723
- const AGENT_RELAY_TOOL_CALL_METADATA = {
724
- add_agent: { toolType: 'agent.create', toolCategory: 'spawn' },
725
- remove_agent: { toolType: 'agent.release', toolCategory: 'release' },
726
- invoke_action: { toolType: 'action.invoke', toolCategory: 'action' },
727
- list_actions: { toolType: 'action.list', toolCategory: 'action' },
728
- submit_result: { toolType: 'result.submit', toolCategory: 'result' },
729
- create_workspace: { toolType: 'workspace.create', toolCategory: 'workspace' },
730
- set_workspace_key: { toolType: 'workspace.set_key', toolCategory: 'workspace' },
731
- register_agent: { toolType: 'agent.register', toolCategory: 'agent' },
732
- list_agents: { toolType: 'agent.list', toolCategory: 'agent' },
733
- post_message: { toolType: 'message.post', toolCategory: 'message' },
734
- send_dm: { toolType: 'message.dm', toolCategory: 'message' },
735
- send_group_dm: { toolType: 'message.group_dm', toolCategory: 'message' },
736
- list_dms: { toolType: 'message.dm_list', toolCategory: 'message' },
737
- list_messages: { toolType: 'message.list', toolCategory: 'message' },
738
- get_message: { toolType: 'message.get', toolCategory: 'message' },
739
- reply_to_thread: { toolType: 'message.reply', toolCategory: 'message' },
740
- get_message_thread: { toolType: 'message.thread', toolCategory: 'message' },
741
- get_thread: { toolType: 'message.thread', toolCategory: 'message' },
742
- search_messages: { toolType: 'message.search', toolCategory: 'message' },
743
- create_channel: { toolType: 'channel.create', toolCategory: 'channel' },
744
- list_channels: { toolType: 'channel.list', toolCategory: 'channel' },
745
- join_channel: { toolType: 'channel.join', toolCategory: 'channel' },
746
- leave_channel: { toolType: 'channel.leave', toolCategory: 'channel' },
747
- set_channel_topic: { toolType: 'channel.set_topic', toolCategory: 'channel' },
748
- archive_channel: { toolType: 'channel.archive', toolCategory: 'channel' },
749
- invite_to_channel: { toolType: 'channel.invite', toolCategory: 'channel' },
750
- list_channel_members: { toolType: 'channel.member_list', toolCategory: 'channel' },
751
- add_reaction: { toolType: 'reaction.add', toolCategory: 'reaction' },
752
- remove_reaction: { toolType: 'reaction.remove', toolCategory: 'reaction' },
753
- check_inbox: { toolType: 'inbox.check', toolCategory: 'inbox' },
754
- mark_message_read: { toolType: 'inbox.mark_read', toolCategory: 'inbox' },
755
- get_message_readers: { toolType: 'inbox.reader_list', toolCategory: 'inbox' },
756
- };
757
- function readInvokedActionName(name, args) {
758
- if (name !== 'invoke_action') {
759
- return undefined;
601
+ function serializableActionSchema(schema) {
602
+ if (isSchemaObject(schema)) {
603
+ return schema;
760
604
  }
761
- const [input] = args;
762
- if (!input || typeof input !== 'object') {
763
- return undefined;
605
+ if (isZodLikeSchema(schema)) {
606
+ return {
607
+ type: 'zod',
608
+ ...(schema.description ? { description: schema.description } : {}),
609
+ };
764
610
  }
765
- const actionName = input.name;
766
- return typeof actionName === 'string' && actionName.trim() ? actionName : undefined;
611
+ return schema;
767
612
  }
768
- function agentRelayActionNameCategory(name) {
769
- const leaf = name.split(/[._-]/).filter(Boolean).at(-1)?.toLowerCase();
770
- switch (leaf) {
771
- case 'create':
772
- case 'spawn':
773
- case 'attach':
774
- return 'spawn';
775
- case 'release':
776
- return 'release';
777
- case 'status':
778
- return 'agent';
779
- default:
780
- return 'action';
613
+ function isZodLikeSchema(schema) {
614
+ return Boolean(schema &&
615
+ typeof schema === 'object' &&
616
+ !Array.isArray(schema) &&
617
+ typeof schema.safeParse === 'function');
618
+ }
619
+ function registerAgentRelayActionTools(server, actions, getSession, onAuditEvent, getAgentClient, actionToolNames) {
620
+ if (!actions) {
621
+ return;
781
622
  }
623
+ /**
624
+ * Fire-and-forget invocation through the relay: returns an immediate ack
625
+ * (with an `invocation_id`) and does NOT run the handler inline. Falls back to
626
+ * the local in-process registry when the relay action surface is unavailable.
627
+ */
628
+ const invokeAction = async (name, input) => {
629
+ const relayActions = getRelayAgentActions(getAgentClient);
630
+ if (relayActions) {
631
+ try {
632
+ const ack = await relayActions.invoke(name, asInputRecord(input));
633
+ return jsonContent({ ok: true, status: 'invoked', invocation: ack });
634
+ }
635
+ catch (error) {
636
+ return { ...jsonContent({ ok: false, error: errorMessage(error) }), isError: true };
637
+ }
638
+ }
639
+ const session = getSession();
640
+ const result = await actions.invoke({
641
+ name,
642
+ input,
643
+ context: {
644
+ caller: { name: session.agentName ?? 'mcp', type: 'agent' },
645
+ emit: onAuditEvent,
646
+ },
647
+ });
648
+ return result.ok ? jsonContent(result) : { ...jsonContent(result), isError: true };
649
+ };
650
+ server.registerTool('list_actions', {
651
+ title: 'List Actions',
652
+ description: 'List Agent Relay actions available to this agent.',
653
+ inputSchema: {},
654
+ outputSchema: jsonResult,
655
+ annotations: {
656
+ readOnlyHint: true,
657
+ destructiveHint: false,
658
+ idempotentHint: true,
659
+ openWorldHint: false,
660
+ },
661
+ }, async () => jsonContent({
662
+ actions: (await actions.list({ visibility: 'agent' })).map(serializableActionDescriptor),
663
+ }));
664
+ server.registerTool('invoke_action', {
665
+ title: 'Invoke Action',
666
+ description: 'Invoke a registered Agent Relay action by name. Fire-and-forget: returns an ack with an invocation id; the result arrives asynchronously to the action handler.',
667
+ inputSchema: {
668
+ name: z.string().describe('Registered action name'),
669
+ input: z.unknown().describe('Action input payload'),
670
+ },
671
+ outputSchema: jsonResult,
672
+ annotations: {
673
+ readOnlyHint: false,
674
+ destructiveHint: false,
675
+ idempotentHint: false,
676
+ openWorldHint: false,
677
+ },
678
+ }, async ({ name, input }) => invokeAction(name, input));
679
+ void actions
680
+ .list({ visibility: 'agent' })
681
+ .then((descriptors) => {
682
+ for (const descriptor of descriptors) {
683
+ actionToolNames?.add(descriptor.name);
684
+ server.registerTool(descriptor.name, {
685
+ title: descriptor.name,
686
+ description: descriptor.description,
687
+ inputSchema: actionToolInputSchema(descriptor.inputSchema),
688
+ outputSchema: jsonResult,
689
+ annotations: {
690
+ readOnlyHint: false,
691
+ destructiveHint: false,
692
+ idempotentHint: false,
693
+ openWorldHint: false,
694
+ },
695
+ }, async (args) => invokeAction(descriptor.name, actionInvocationInput(descriptor, args)));
696
+ }
697
+ })
698
+ .catch(() => undefined);
782
699
  }
783
- function agentRelayToolCallMetadata(name, args, actionToolNames) {
784
- const invokedActionName = readInvokedActionName(name, args);
785
- if (invokedActionName) {
786
- return {
787
- toolType: invokedActionName,
788
- toolCategory: agentRelayActionNameCategory(invokedActionName),
789
- };
700
+ /** The relay-backed action surface on the live agent client, when available. */
701
+ function getRelayAgentActions(getAgentClient) {
702
+ if (!getAgentClient) {
703
+ return undefined;
704
+ }
705
+ try {
706
+ return getAgentClient().actions;
790
707
  }
791
- const known = AGENT_RELAY_TOOL_CALL_METADATA[name];
792
- if (known) {
793
- return known;
708
+ catch {
709
+ return undefined;
794
710
  }
795
- if (actionToolNames.has(name)) {
796
- return {
797
- toolType: name,
798
- toolCategory: agentRelayActionNameCategory(name),
799
- };
711
+ }
712
+ function asInputRecord(input) {
713
+ if (input === undefined || input === null) {
714
+ return undefined;
715
+ }
716
+ if (typeof input === 'object' && !Array.isArray(input)) {
717
+ return input;
800
718
  }
801
- return { toolType: name, toolCategory: 'tool' };
719
+ return { input };
802
720
  }
803
- function isErrorToolResult(value) {
804
- return Boolean(value && typeof value === 'object' && value.isError === true);
721
+ function errorMessage(error) {
722
+ return error instanceof Error ? error.message : String(error);
805
723
  }
806
- function trackAgentRelayToolCall(input) {
807
- track('agent_relay_tool_call', {
808
- tool_name: input.toolName,
809
- tool_type: input.toolType,
810
- tool_category: input.toolCategory,
811
- transport: input.transport ?? 'unknown',
812
- success: input.success,
813
- duration_ms: Date.now() - input.startedAt,
814
- ...(input.errorClass ? { error_class: input.errorClass } : {}),
815
- });
724
+ function createRegisteredAgent(agentName, agentToken) {
725
+ return { agentName, agentToken };
816
726
  }
817
- function enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken, telemetryTransport, actionToolNames = new Set()) {
818
- const original = mcpServer.registerTool.bind(mcpServer);
819
- const mutableServer = mcpServer;
820
- mutableServer.registerTool = (name, config, handler) => {
821
- if (!handler) {
822
- return original(name, config, handler);
727
+ export async function registerAgentWithRebind({ session, setSession, getRelay, name, type, persona, metadata, strictAgentName, preferredAgentName, forcedAgentType, }) {
728
+ requireWorkspaceKey(session);
729
+ const configuredName = session.agentName ?? preferredAgentName?.trim() ?? null;
730
+ const warnings = [];
731
+ const effectiveName = strictAgentName && configuredName ? configuredName : name;
732
+ if (strictAgentName && configuredName && name.trim() !== configuredName) {
733
+ warnings.push(`Strict worker identity is enabled; ignoring requested name "${name}" and using "${configuredName}".`);
734
+ }
735
+ const effectiveType = forcedAgentType ?? type;
736
+ if (forcedAgentType && type && type !== forcedAgentType) {
737
+ warnings.push(`Forced worker type is enabled; ignoring requested type "${type}" and using "${forcedAgentType}".`);
738
+ }
739
+ if (session.agentToken && effectiveName && strictAgentName) {
740
+ // If the session tracks per-identity agents, only short-circuit when the
741
+ // strict-named identity is still registered. After an `agent_token_invalid`
742
+ // recovery the entry is dropped from the map, which lets this fall through
743
+ // to a fresh registerOrRotate instead of handing back the dead token.
744
+ const cachedAgent = session.agents?.get(effectiveName);
745
+ const knowsIdentities = session.agents !== undefined;
746
+ if (!knowsIdentities || cachedAgent) {
747
+ return {
748
+ name: effectiveName,
749
+ token: cachedAgent?.agentToken ?? session.agentToken,
750
+ registered_name: effectiveName,
751
+ warnings,
752
+ };
823
753
  }
824
- const wrapped = async (...args) => {
825
- const asIdentity = readAsIdentity(args);
826
- const startedAt = Date.now();
827
- const toolMetadata = agentRelayToolCallMetadata(name, args, actionToolNames);
828
- let result;
829
- try {
830
- result = await handler(...args);
831
- }
832
- catch (err) {
833
- // `register_agent` is the recovery path itself — never invalidate a
834
- // freshly-issued token, and let registration errors bubble normally.
835
- if (name !== 'register_agent' && isInvalidAgentTokenError(err)) {
836
- invalidateAgentToken(asIdentity);
837
- trackAgentRelayToolCall({
838
- toolName: name,
839
- toolType: toolMetadata.toolType,
840
- toolCategory: toolMetadata.toolCategory,
841
- transport: telemetryTransport,
842
- startedAt,
843
- success: false,
844
- errorClass: errorClassName(err) ?? 'InvalidAgentToken',
845
- });
846
- return invalidAgentTokenToolResult();
847
- }
848
- trackAgentRelayToolCall({
849
- toolName: name,
850
- toolType: toolMetadata.toolType,
851
- toolCategory: toolMetadata.toolCategory,
852
- transport: telemetryTransport,
853
- startedAt,
854
- success: false,
855
- errorClass: errorClassName(err),
856
- });
857
- throw err;
858
- }
859
- // Successful response that still carries an "Invalid agent token" body.
860
- if (name !== 'register_agent' && isInvalidAgentTokenToolResult(result)) {
861
- invalidateAgentToken(asIdentity);
862
- trackAgentRelayToolCall({
863
- toolName: name,
864
- toolType: toolMetadata.toolType,
865
- toolCategory: toolMetadata.toolCategory,
866
- transport: telemetryTransport,
867
- startedAt,
868
- success: false,
869
- errorClass: 'InvalidAgentToken',
870
- });
871
- if (hasContentArray(result)) {
872
- result.content.push({ type: 'text', text: agentTokenRecoveryMessage() });
873
- }
874
- return result;
875
- }
876
- if (!SKIP_PIGGYBACK.has(name) && getSession().agentToken && hasContentArray(result)) {
877
- try {
878
- const inbox = await getAgentClient(asIdentity).inbox();
879
- const inboxText = formatInbox(inbox, asIdentity ?? getSession().agentName);
880
- if (inboxText) {
881
- result.content.push({ type: 'text', text: inboxText });
882
- }
883
- }
884
- catch (err) {
885
- // Inbox piggyback is opportunistic; the original tool result should
886
- // still win. But if the inbox fetch itself reveals an invalid token,
887
- // clear the stale identity so the next call doesn't reuse it.
888
- if (isInvalidAgentTokenError(err)) {
889
- invalidateAgentToken(asIdentity);
890
- }
891
- }
892
- }
893
- const resultIsError = isErrorToolResult(result);
894
- trackAgentRelayToolCall({
895
- toolName: name,
896
- toolType: toolMetadata.toolType,
897
- toolCategory: toolMetadata.toolCategory,
898
- transport: telemetryTransport,
899
- startedAt,
900
- success: !resultIsError,
901
- ...(resultIsError ? { errorClass: 'ToolResultError' } : {}),
902
- });
903
- return result;
904
- };
905
- return original(name, config, wrapped);
754
+ }
755
+ const relay = getRelay();
756
+ const result = await relay.agents.registerOrRotate({
757
+ name: effectiveName,
758
+ type: effectiveType,
759
+ persona,
760
+ metadata,
761
+ });
762
+ const reboundName = result.name?.trim() ? result.name : effectiveName;
763
+ setSession({ agentToken: result.token, agentName: reboundName });
764
+ return {
765
+ ...result,
766
+ registered_name: reboundName,
767
+ warnings,
906
768
  };
907
769
  }
908
770
  function resolveEmoji(input) {
@@ -1047,6 +909,23 @@ function registerAgentRelayTools(server, getRelay, getAgentClient, getSession, s
1047
909
  const agents = await getRelay().agents.list(status ? { status } : undefined);
1048
910
  return jsonContent({ agents });
1049
911
  });
912
+ server.registerTool('query_nodes', {
913
+ title: 'Query Fleet Nodes',
914
+ description: 'Query registered fleet nodes by capability or name.',
915
+ inputSchema: {
916
+ capability: z.string().optional().describe('Optional capability name filter'),
917
+ name: z.string().optional().describe('Optional node name filter'),
918
+ },
919
+ outputSchema: {
920
+ nodes: z.array(z.object({}).passthrough()).describe('Fleet nodes'),
921
+ },
922
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
923
+ }, async ({ capability, name }) => {
924
+ const session = getSession();
925
+ requireWorkspaceKey(session);
926
+ const relay = new AgentRelay({ workspaceKey: session.workspaceKey ?? undefined, baseUrl });
927
+ return jsonContent({ nodes: await relay.nodes.list({ capability, name }) });
928
+ });
1050
929
  server.registerTool('create_channel', {
1051
930
  title: 'Create Channel',
1052
931
  description: 'Create a new workspace channel.',
@@ -1355,10 +1234,47 @@ function registerAgentRelayTools(server, getRelay, getAgentClient, getSession, s
1355
1234
  task,
1356
1235
  channel,
1357
1236
  persona,
1358
- // Pass model as a first-class spawn field so it reaches the broker and
1359
- // the launched CLI (--model), not just the agent's display metadata.
1360
- model: model ?? undefined,
1237
+ // SpawnAgentRequest has no top-level model field; pass via metadata
1238
+ // so the broker can extract it and forward --model to the launched CLI.
1239
+ metadata: model ? { model } : undefined,
1361
1240
  })));
1241
+ server.registerTool('spawn', {
1242
+ title: 'Spawn Agent',
1243
+ description: 'Invoke the fleet spawn action. Optionally target a specific node.',
1244
+ inputSchema: {
1245
+ name: z.string().describe('Agent name'),
1246
+ cli: z.enum(['claude', 'codex', 'gemini', 'aider', 'goose']).describe('AI CLI to launch'),
1247
+ task: z.string().optional().describe('Initial task instructions'),
1248
+ channel: z.string().optional().describe('Channel to join'),
1249
+ channels: z.array(z.string()).optional().describe('Channels to join'),
1250
+ model: z.string().optional().describe('Model powering the worker'),
1251
+ session_ref: z.string().optional().describe('Session reference for resumable spawns'),
1252
+ target_node: z.string().optional().describe('Optional target fleet node name'),
1253
+ ...identityOverrideInputShape,
1254
+ },
1255
+ outputSchema: jsonResult,
1256
+ annotations: {
1257
+ readOnlyHint: false,
1258
+ destructiveHint: false,
1259
+ idempotentHint: false,
1260
+ openWorldHint: true,
1261
+ },
1262
+ }, async ({ name, cli, task, channel, channels, model, session_ref, target_node, as }) => {
1263
+ const actions = getAgentClient(as).actions;
1264
+ if (!actions) {
1265
+ throw new Error('spawn requires an agent-scoped Relaycast actions client.');
1266
+ }
1267
+ const actionInput = {
1268
+ name,
1269
+ cli,
1270
+ ...(task ? { task } : {}),
1271
+ ...(model ? { model } : {}),
1272
+ ...(session_ref ? { session_ref } : {}),
1273
+ ...(target_node ? { target_node } : {}),
1274
+ ...((channels ?? (channel ? [channel] : undefined)) ? { channels: channels ?? [channel] } : {}),
1275
+ };
1276
+ return jsonContent({ invocation: await actions.invoke('spawn', actionInput) });
1277
+ });
1362
1278
  server.registerTool('remove_agent', {
1363
1279
  title: 'Remove Agent',
1364
1280
  description: 'Release a worker agent from active duty.',
@@ -1430,10 +1346,10 @@ export function createAgentRelayMcpServer(options) {
1430
1346
  if (session.agentToken && !session.wsBridge && !session.wsInitAttempted) {
1431
1347
  try {
1432
1348
  const subscriptions = new SubscriptionManager();
1433
- const wsClient = new WsClient(withRelaycastTelemetry({
1349
+ const wsClient = new WsClient({
1434
1350
  token: session.agentToken,
1435
1351
  baseUrl: options.baseUrl,
1436
- }));
1352
+ });
1437
1353
  const wsBridge = new RealtimeResourceBridge(wsClient, subscriptions, (uri) => {
1438
1354
  mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined);
1439
1355
  });
@@ -1457,9 +1373,6 @@ export function createAgentRelayMcpServer(options) {
1457
1373
  nextAgents.delete(targetName);
1458
1374
  partial.agents = nextAgents;
1459
1375
  }
1460
- // Clear the active-session token when the invalidated identity is the
1461
- // active one (or the caller didn't pin to a particular identity). The
1462
- // active workspaceKey stays intact so `register_agent` can recover.
1463
1376
  if (!asIdentity || asIdentity === session.agentName) {
1464
1377
  if (session.agentToken !== null) {
1465
1378
  partial.agentToken = null;
@@ -1492,7 +1405,7 @@ export function createAgentRelayMcpServer(options) {
1492
1405
  baseUrl: options.baseUrl,
1493
1406
  })).as(agentToken, { autoHeartbeatMs: false });
1494
1407
  };
1495
- enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken, options.telemetryTransport, actionToolNames);
1408
+ enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken);
1496
1409
  registerResourceDefinitions(mcpServer, getAgentClient, getRelay);
1497
1410
  mcpServer.server.setRequestHandler(SubscribeRequestSchema, async (req) => {
1498
1411
  session.subscriptions?.subscribe(req.params.uri);
@@ -1536,9 +1449,6 @@ export function createAgentRelayMcpServer(options) {
1536
1449
  return result;
1537
1450
  });
1538
1451
  }
1539
- if (session.agentToken && !session.wsBridge) {
1540
- setSession({ agentToken: session.agentToken, agentName: session.agentName });
1541
- }
1542
1452
  return mcpServer;
1543
1453
  }
1544
1454
  /** Relaycast agent tokens are opaque `at_live_<hex>` literals. Anything else