agent-relay 8.7.2 → 8.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto/classifier.d.ts +29 -0
- package/dist/auto/classifier.d.ts.map +1 -0
- package/dist/auto/classifier.js +126 -0
- package/dist/auto/classifier.js.map +1 -0
- package/dist/auto/composer.d.ts +105 -0
- package/dist/auto/composer.d.ts.map +1 -0
- package/dist/auto/composer.js +454 -0
- package/dist/auto/composer.js.map +1 -0
- package/dist/auto/director-prompt.d.ts +25 -0
- package/dist/auto/director-prompt.d.ts.map +1 -0
- package/dist/auto/director-prompt.js +53 -0
- package/dist/auto/director-prompt.js.map +1 -0
- package/dist/auto/index.d.ts +15 -0
- package/dist/auto/index.d.ts.map +1 -0
- package/dist/auto/index.js +13 -0
- package/dist/auto/index.js.map +1 -0
- package/dist/cli/agent-relay-mcp.d.ts +1 -1
- package/dist/cli/agent-relay-mcp.d.ts.map +1 -1
- package/dist/cli/agent-relay-mcp.js +670 -610
- package/dist/cli/agent-relay-mcp.js.map +1 -1
- package/dist/cli/bootstrap.d.ts.map +1 -1
- package/dist/cli/bootstrap.js +2 -0
- package/dist/cli/bootstrap.js.map +1 -1
- package/dist/cli/commands/core.d.ts +1 -0
- package/dist/cli/commands/core.d.ts.map +1 -1
- package/dist/cli/commands/core.js +1 -1
- package/dist/cli/commands/core.js.map +1 -1
- package/dist/cli/commands/fleet.d.ts +16 -0
- package/dist/cli/commands/fleet.d.ts.map +1 -0
- package/dist/cli/commands/fleet.js +188 -0
- package/dist/cli/commands/fleet.js.map +1 -0
- package/dist/cli/commands/local-agent.d.ts.map +1 -1
- package/dist/cli/commands/local-agent.js +71 -13
- package/dist/cli/commands/local-agent.js.map +1 -1
- package/dist/cli/lib/broker-lifecycle.d.ts +5 -1
- package/dist/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/cli/lib/broker-lifecycle.js +32 -2
- package/dist/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/cli/lib/client-factory.d.ts +2 -0
- package/dist/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/cli/lib/client-factory.js.map +1 -1
- package/dist/cli/lib/fleet-sidecar.d.ts +53 -0
- package/dist/cli/lib/fleet-sidecar.d.ts.map +1 -0
- package/dist/cli/lib/fleet-sidecar.js +400 -0
- package/dist/cli/lib/fleet-sidecar.js.map +1 -0
- package/dist/index.cjs +2545 -16773
- package/package.json +10 -8
|
@@ -7,6 +7,7 @@ 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
12
|
import { initTelemetry, shutdown as shutdownTelemetry, track, } from './telemetry/index.js';
|
|
12
13
|
import { relaycastWorkspaceTelemetryOptions, withRelaycastTelemetry } from './lib/relaycast-telemetry.js';
|
|
@@ -14,6 +15,11 @@ import { errorClassName } from './lib/telemetry-helpers.js';
|
|
|
14
15
|
const DEFAULT_BASE_URL = 'https://gateway.relaycast.dev';
|
|
15
16
|
export const AGENT_RELAY_MCP_VERSION = process.env.AGENT_RELAY_CLI_VERSION ?? SDK_VERSION ?? 'unknown';
|
|
16
17
|
let mcpTelemetryExitHookInstalled = false;
|
|
18
|
+
const EXIT_AFTER_TASK_INSTRUCTION = '## Post-task exit\n' +
|
|
19
|
+
'When the requested task is fully complete and you have reported the final outcome, output `/exit` on its own line so the Agent Relay harness exits cleanly. Do not output `/exit` before the task is complete.';
|
|
20
|
+
function withExitAfterTaskInstruction(task) {
|
|
21
|
+
return `${task}\n\n${EXIT_AFTER_TASK_INSTRUCTION}`;
|
|
22
|
+
}
|
|
17
23
|
const DEFAULT_SYSTEM_PROMPT = `You are an AI agent in a collaborative workspace powered by Agent Relay. You can communicate with other agents using these MCP tools:
|
|
18
24
|
|
|
19
25
|
## Getting Started
|
|
@@ -30,6 +36,10 @@ const DEFAULT_SYSTEM_PROMPT = `You are an AI agent in a collaborative workspace
|
|
|
30
36
|
- Reply to threads with "reply_to_thread"
|
|
31
37
|
- React to messages with "add_reaction"
|
|
32
38
|
|
|
39
|
+
## Fleet
|
|
40
|
+
- Use "query_nodes" to find fleet nodes by capability or name
|
|
41
|
+
- Use "spawn" to invoke the fleet spawn action on an eligible node
|
|
42
|
+
|
|
33
43
|
## Best Practices
|
|
34
44
|
- Check your inbox regularly for new messages and mentions
|
|
35
45
|
- Use channels for topic-based discussions
|
|
@@ -207,412 +217,95 @@ function createInitialSession(options) {
|
|
|
207
217
|
wsInitAttempted: false,
|
|
208
218
|
};
|
|
209
219
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
220
|
+
class SubscriptionManager {
|
|
221
|
+
subscriptions = new Set();
|
|
222
|
+
subscribe(uri) {
|
|
223
|
+
this.subscriptions.add(uri);
|
|
313
224
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
input: z.unknown().describe('Action input payload. The action registry performs final validation.'),
|
|
317
|
-
};
|
|
225
|
+
unsubscribe(uri) {
|
|
226
|
+
this.subscriptions.delete(uri);
|
|
318
227
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
322
|
-
const child = zodFromJsonSchema(childSchema);
|
|
323
|
-
shape[key] = required.has(key) ? child : child.optional();
|
|
228
|
+
getMatchingSubscriptions(uris) {
|
|
229
|
+
return uris.filter((uri) => this.subscriptions.has(uri));
|
|
324
230
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
function actionInvocationInput(descriptor, args) {
|
|
328
|
-
const schema = descriptor.inputSchema;
|
|
329
|
-
if (zodObjectShape(schema)) {
|
|
330
|
-
return args;
|
|
231
|
+
getAll() {
|
|
232
|
+
return [...this.subscriptions];
|
|
331
233
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
? args.input
|
|
335
|
-
: args;
|
|
234
|
+
clear() {
|
|
235
|
+
this.subscriptions.clear();
|
|
336
236
|
}
|
|
337
|
-
return args;
|
|
338
237
|
}
|
|
339
|
-
function
|
|
340
|
-
if (
|
|
341
|
-
return
|
|
238
|
+
function getStringEventField(event, field) {
|
|
239
|
+
if (typeof event !== 'object' || event === null) {
|
|
240
|
+
return null;
|
|
342
241
|
}
|
|
343
|
-
|
|
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
|
-
};
|
|
242
|
+
const candidate = event[field];
|
|
243
|
+
return typeof candidate === 'string' ? candidate : null;
|
|
353
244
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
245
|
+
function eventToResourceUris(event) {
|
|
246
|
+
const type = getStringEventField(event, 'type');
|
|
247
|
+
switch (type) {
|
|
248
|
+
case 'message.created': {
|
|
249
|
+
const channel = getStringEventField(event, 'channel');
|
|
250
|
+
return channel ? ['relay://inbox', `relay://channels/${channel}/messages`] : ['relay://inbox'];
|
|
251
|
+
}
|
|
252
|
+
case 'message.updated': {
|
|
253
|
+
const channel = getStringEventField(event, 'channel');
|
|
254
|
+
return channel ? [`relay://channels/${channel}/messages`] : [];
|
|
255
|
+
}
|
|
256
|
+
case 'thread.reply': {
|
|
257
|
+
const parentId = getStringEventField(event, 'parentId');
|
|
258
|
+
return parentId ? ['relay://inbox', `relay://messages/${parentId}/thread`] : ['relay://inbox'];
|
|
259
|
+
}
|
|
260
|
+
case 'dm.received':
|
|
261
|
+
case 'group_dm.received': {
|
|
262
|
+
const conversationId = getStringEventField(event, 'conversationId');
|
|
263
|
+
return conversationId ? ['relay://inbox', `relay://dm/${conversationId}`] : ['relay://inbox'];
|
|
264
|
+
}
|
|
265
|
+
case 'agent.online':
|
|
266
|
+
case 'agent.offline':
|
|
267
|
+
return ['relay://agents'];
|
|
268
|
+
case 'channel.created':
|
|
269
|
+
case 'channel.updated':
|
|
270
|
+
case 'channel.archived':
|
|
271
|
+
case 'member.joined':
|
|
272
|
+
case 'member.left':
|
|
273
|
+
return ['relay://channels'];
|
|
274
|
+
case 'webhook.received':
|
|
275
|
+
case 'command.invoked': {
|
|
276
|
+
const channel = getStringEventField(event, 'channel');
|
|
277
|
+
return channel ? [`relay://channels/${channel}/messages`] : [];
|
|
278
|
+
}
|
|
279
|
+
case 'reaction.added':
|
|
280
|
+
case 'reaction.removed':
|
|
281
|
+
return ['relay://inbox'];
|
|
282
|
+
default:
|
|
283
|
+
return [];
|
|
363
284
|
}
|
|
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
285
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
286
|
+
class RealtimeResourceBridge {
|
|
287
|
+
wsClient;
|
|
288
|
+
subscriptions;
|
|
289
|
+
notifyCallback;
|
|
290
|
+
unsubscribeFn = null;
|
|
291
|
+
constructor(wsClient, subscriptions, notifyCallback) {
|
|
292
|
+
this.wsClient = wsClient;
|
|
293
|
+
this.subscriptions = subscriptions;
|
|
294
|
+
this.notifyCallback = notifyCallback;
|
|
375
295
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const ack = await relayActions.invoke(name, asInputRecord(input));
|
|
386
|
-
return jsonContent({ ok: true, status: 'invoked', invocation: ack });
|
|
296
|
+
start() {
|
|
297
|
+
this.unsubscribeFn = this.wsClient.on('*', (event) => {
|
|
298
|
+
const type = getStringEventField(event, 'type');
|
|
299
|
+
if (type === 'open' ||
|
|
300
|
+
type === 'close' ||
|
|
301
|
+
type === 'error' ||
|
|
302
|
+
type === 'reconnecting' ||
|
|
303
|
+
type === 'permanently_disconnected') {
|
|
304
|
+
return;
|
|
387
305
|
}
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
class SubscriptionManager {
|
|
524
|
-
subscriptions = new Set();
|
|
525
|
-
subscribe(uri) {
|
|
526
|
-
this.subscriptions.add(uri);
|
|
527
|
-
}
|
|
528
|
-
unsubscribe(uri) {
|
|
529
|
-
this.subscriptions.delete(uri);
|
|
530
|
-
}
|
|
531
|
-
getMatchingSubscriptions(uris) {
|
|
532
|
-
return uris.filter((uri) => this.subscriptions.has(uri));
|
|
533
|
-
}
|
|
534
|
-
getAll() {
|
|
535
|
-
return [...this.subscriptions];
|
|
536
|
-
}
|
|
537
|
-
clear() {
|
|
538
|
-
this.subscriptions.clear();
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
function getStringEventField(event, field) {
|
|
542
|
-
if (typeof event !== 'object' || event === null) {
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
const candidate = event[field];
|
|
546
|
-
return typeof candidate === 'string' ? candidate : null;
|
|
547
|
-
}
|
|
548
|
-
function eventToResourceUris(event) {
|
|
549
|
-
const type = getStringEventField(event, 'type');
|
|
550
|
-
switch (type) {
|
|
551
|
-
case 'message.created': {
|
|
552
|
-
const channel = getStringEventField(event, 'channel');
|
|
553
|
-
return channel ? ['relay://inbox', `relay://channels/${channel}/messages`] : ['relay://inbox'];
|
|
554
|
-
}
|
|
555
|
-
case 'message.updated': {
|
|
556
|
-
const channel = getStringEventField(event, 'channel');
|
|
557
|
-
return channel ? [`relay://channels/${channel}/messages`] : [];
|
|
558
|
-
}
|
|
559
|
-
case 'thread.reply': {
|
|
560
|
-
const parentId = getStringEventField(event, 'parentId');
|
|
561
|
-
return parentId ? ['relay://inbox', `relay://messages/${parentId}/thread`] : ['relay://inbox'];
|
|
562
|
-
}
|
|
563
|
-
case 'dm.received':
|
|
564
|
-
case 'group_dm.received': {
|
|
565
|
-
const conversationId = getStringEventField(event, 'conversationId');
|
|
566
|
-
return conversationId ? ['relay://inbox', `relay://dm/${conversationId}`] : ['relay://inbox'];
|
|
567
|
-
}
|
|
568
|
-
case 'agent.online':
|
|
569
|
-
case 'agent.offline':
|
|
570
|
-
return ['relay://agents'];
|
|
571
|
-
case 'channel.created':
|
|
572
|
-
case 'channel.updated':
|
|
573
|
-
case 'channel.archived':
|
|
574
|
-
case 'member.joined':
|
|
575
|
-
case 'member.left':
|
|
576
|
-
return ['relay://channels'];
|
|
577
|
-
case 'webhook.received': {
|
|
578
|
-
const channel = getStringEventField(event, 'channel');
|
|
579
|
-
return channel ? [`relay://channels/${channel}/messages`] : [];
|
|
580
|
-
}
|
|
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
|
-
case 'reaction.added':
|
|
587
|
-
case 'reaction.removed':
|
|
588
|
-
return ['relay://inbox'];
|
|
589
|
-
default:
|
|
590
|
-
return [];
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
class RealtimeResourceBridge {
|
|
594
|
-
wsClient;
|
|
595
|
-
subscriptions;
|
|
596
|
-
notifyCallback;
|
|
597
|
-
unsubscribeFn = null;
|
|
598
|
-
constructor(wsClient, subscriptions, notifyCallback) {
|
|
599
|
-
this.wsClient = wsClient;
|
|
600
|
-
this.subscriptions = subscriptions;
|
|
601
|
-
this.notifyCallback = notifyCallback;
|
|
602
|
-
}
|
|
603
|
-
start() {
|
|
604
|
-
this.unsubscribeFn = this.wsClient.on('*', (event) => {
|
|
605
|
-
const type = getStringEventField(event, 'type');
|
|
606
|
-
if (type === 'open' ||
|
|
607
|
-
type === 'close' ||
|
|
608
|
-
type === 'error' ||
|
|
609
|
-
type === 'reconnecting' ||
|
|
610
|
-
type === 'permanently_disconnected') {
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
const matched = this.subscriptions.getMatchingSubscriptions(eventToResourceUris(event));
|
|
614
|
-
for (const uri of matched) {
|
|
615
|
-
this.notifyCallback(uri);
|
|
306
|
+
const matched = this.subscriptions.getMatchingSubscriptions(eventToResourceUris(event));
|
|
307
|
+
for (const uri of matched) {
|
|
308
|
+
this.notifyCallback(uri);
|
|
616
309
|
}
|
|
617
310
|
});
|
|
618
311
|
this.wsClient.connect();
|
|
@@ -692,217 +385,506 @@ function formatInbox(inbox, selfName) {
|
|
|
692
385
|
lines.push(` From ${dm.from}: ${dm.unreadCount} unread`);
|
|
693
386
|
}
|
|
694
387
|
}
|
|
695
|
-
const reactions = selfNorm
|
|
696
|
-
? inbox.recentReactions?.filter((reaction) => !isSelf(reaction.agentName))
|
|
697
|
-
: inbox.recentReactions;
|
|
698
|
-
if (reactions?.length) {
|
|
699
|
-
lines.push('Reactions (informational; no response required):');
|
|
700
|
-
for (const reaction of reactions) {
|
|
701
|
-
lines.push(` :${reaction.emoji}: on your message in #${reaction.channelName} by @${reaction.agentName}`);
|
|
702
|
-
}
|
|
388
|
+
const reactions = selfNorm
|
|
389
|
+
? inbox.recentReactions?.filter((reaction) => !isSelf(reaction.agentName))
|
|
390
|
+
: inbox.recentReactions;
|
|
391
|
+
if (reactions?.length) {
|
|
392
|
+
lines.push('Reactions (informational; no response required):');
|
|
393
|
+
for (const reaction of reactions) {
|
|
394
|
+
lines.push(` :${reaction.emoji}: on your message in #${reaction.channelName} by @${reaction.agentName}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return lines.length === 1 ? '' : lines.join('\n');
|
|
398
|
+
}
|
|
399
|
+
function readAsIdentity(args) {
|
|
400
|
+
const [input] = args;
|
|
401
|
+
if (typeof input !== 'object' || input === null)
|
|
402
|
+
return undefined;
|
|
403
|
+
const as = input.as;
|
|
404
|
+
return typeof as === 'string' ? as : undefined;
|
|
405
|
+
}
|
|
406
|
+
function invalidAgentTokenToolResult() {
|
|
407
|
+
const text = agentTokenRecoveryMessage();
|
|
408
|
+
return {
|
|
409
|
+
content: [{ type: 'text', text }],
|
|
410
|
+
structuredContent: {
|
|
411
|
+
error: { code: INVALID_AGENT_TOKEN_CODE, message: text },
|
|
412
|
+
},
|
|
413
|
+
isError: true,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function isErrorToolResult(value) {
|
|
417
|
+
return Boolean(value && typeof value === 'object' && value.isError === true);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Owned tools that delegate to the actions surface (`actions.invoke(...)`)
|
|
421
|
+
* rather than the agents/messaging APIs. Together with the dynamic per-action
|
|
422
|
+
* tools (tracked via `actionToolNames`), these intentionally skip per-tool
|
|
423
|
+
* telemetry so the same underlying action is not counted differently depending
|
|
424
|
+
* on which MCP surface the caller used (e.g. `spawn` vs `invoke_action`).
|
|
425
|
+
*/
|
|
426
|
+
const ACTION_ROUTED_TOOL_NAMES = new Set(['invoke_action', 'spawn']);
|
|
427
|
+
/**
|
|
428
|
+
* Coarse type/category metadata for the statically-registered ("owned") MCP
|
|
429
|
+
* tools. Action-routed calls (see `ACTION_ROUTED_TOOL_NAMES`) and the dynamic
|
|
430
|
+
* per-action tools surfaced from the actions registry are intentionally
|
|
431
|
+
* excluded from per-tool telemetry (see the skip in `enableInboxPiggyback`),
|
|
432
|
+
* so they have no entry.
|
|
433
|
+
*/
|
|
434
|
+
const AGENT_RELAY_TOOL_CALL_METADATA = {
|
|
435
|
+
add_agent: { toolType: 'agent.create', toolCategory: 'spawn' },
|
|
436
|
+
remove_agent: { toolType: 'agent.release', toolCategory: 'release' },
|
|
437
|
+
list_actions: { toolType: 'action.list', toolCategory: 'action' },
|
|
438
|
+
submit_result: { toolType: 'result.submit', toolCategory: 'result' },
|
|
439
|
+
create_workspace: { toolType: 'workspace.create', toolCategory: 'workspace' },
|
|
440
|
+
set_workspace_key: { toolType: 'workspace.set_key', toolCategory: 'workspace' },
|
|
441
|
+
register_agent: { toolType: 'agent.register', toolCategory: 'agent' },
|
|
442
|
+
list_agents: { toolType: 'agent.list', toolCategory: 'agent' },
|
|
443
|
+
post_message: { toolType: 'message.post', toolCategory: 'message' },
|
|
444
|
+
send_dm: { toolType: 'message.dm', toolCategory: 'message' },
|
|
445
|
+
send_group_dm: { toolType: 'message.group_dm', toolCategory: 'message' },
|
|
446
|
+
list_dms: { toolType: 'message.dm_list', toolCategory: 'message' },
|
|
447
|
+
list_messages: { toolType: 'message.list', toolCategory: 'message' },
|
|
448
|
+
reply_to_thread: { toolType: 'message.reply', toolCategory: 'message' },
|
|
449
|
+
get_message_thread: { toolType: 'message.thread', toolCategory: 'message' },
|
|
450
|
+
search_messages: { toolType: 'message.search', toolCategory: 'message' },
|
|
451
|
+
create_channel: { toolType: 'channel.create', toolCategory: 'channel' },
|
|
452
|
+
list_channels: { toolType: 'channel.list', toolCategory: 'channel' },
|
|
453
|
+
join_channel: { toolType: 'channel.join', toolCategory: 'channel' },
|
|
454
|
+
leave_channel: { toolType: 'channel.leave', toolCategory: 'channel' },
|
|
455
|
+
set_channel_topic: { toolType: 'channel.set_topic', toolCategory: 'channel' },
|
|
456
|
+
archive_channel: { toolType: 'channel.archive', toolCategory: 'channel' },
|
|
457
|
+
invite_to_channel: { toolType: 'channel.invite', toolCategory: 'channel' },
|
|
458
|
+
add_reaction: { toolType: 'reaction.add', toolCategory: 'reaction' },
|
|
459
|
+
remove_reaction: { toolType: 'reaction.remove', toolCategory: 'reaction' },
|
|
460
|
+
check_inbox: { toolType: 'inbox.check', toolCategory: 'inbox' },
|
|
461
|
+
mark_message_read: { toolType: 'inbox.mark_read', toolCategory: 'inbox' },
|
|
462
|
+
get_message_readers: { toolType: 'inbox.reader_list', toolCategory: 'inbox' },
|
|
463
|
+
};
|
|
464
|
+
function agentRelayToolCallMetadata(name) {
|
|
465
|
+
const known = AGENT_RELAY_TOOL_CALL_METADATA[name];
|
|
466
|
+
return known ?? { toolType: name, toolCategory: 'tool' };
|
|
467
|
+
}
|
|
468
|
+
function trackAgentRelayToolCall(input) {
|
|
469
|
+
track('agent_relay_tool_call', {
|
|
470
|
+
tool_name: input.toolName,
|
|
471
|
+
tool_type: input.toolType,
|
|
472
|
+
tool_category: input.toolCategory,
|
|
473
|
+
transport: input.transport ?? 'unknown',
|
|
474
|
+
success: input.success,
|
|
475
|
+
duration_ms: Date.now() - input.startedAt,
|
|
476
|
+
...(input.errorClass ? { error_class: input.errorClass } : {}),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken, telemetryTransport, actionToolNames = new Set()) {
|
|
480
|
+
const original = mcpServer.registerTool.bind(mcpServer);
|
|
481
|
+
const mutableServer = mcpServer;
|
|
482
|
+
mutableServer.registerTool = (name, config, handler) => {
|
|
483
|
+
if (!handler) {
|
|
484
|
+
return original(name, config, handler);
|
|
485
|
+
}
|
|
486
|
+
const wrapped = async (...args) => {
|
|
487
|
+
const asIdentity = readAsIdentity(args);
|
|
488
|
+
const startedAt = Date.now();
|
|
489
|
+
// Action-routed calls (`invoke_action`, `spawn`, and the dynamic
|
|
490
|
+
// per-action tools) run through the actions surface and deliberately skip
|
|
491
|
+
// per-tool telemetry; only the owned tools emit `agent_relay_tool_call`.
|
|
492
|
+
const toolMetadata = !ACTION_ROUTED_TOOL_NAMES.has(name) && !actionToolNames.has(name)
|
|
493
|
+
? agentRelayToolCallMetadata(name)
|
|
494
|
+
: undefined;
|
|
495
|
+
let result;
|
|
496
|
+
try {
|
|
497
|
+
result = await handler(...args);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
if (name !== 'register_agent' && isInvalidAgentTokenError(err)) {
|
|
501
|
+
invalidateAgentToken(asIdentity);
|
|
502
|
+
if (toolMetadata) {
|
|
503
|
+
trackAgentRelayToolCall({
|
|
504
|
+
toolName: name,
|
|
505
|
+
toolType: toolMetadata.toolType,
|
|
506
|
+
toolCategory: toolMetadata.toolCategory,
|
|
507
|
+
transport: telemetryTransport,
|
|
508
|
+
startedAt,
|
|
509
|
+
success: false,
|
|
510
|
+
errorClass: errorClassName(err) ?? 'InvalidAgentToken',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return invalidAgentTokenToolResult();
|
|
514
|
+
}
|
|
515
|
+
if (toolMetadata) {
|
|
516
|
+
trackAgentRelayToolCall({
|
|
517
|
+
toolName: name,
|
|
518
|
+
toolType: toolMetadata.toolType,
|
|
519
|
+
toolCategory: toolMetadata.toolCategory,
|
|
520
|
+
transport: telemetryTransport,
|
|
521
|
+
startedAt,
|
|
522
|
+
success: false,
|
|
523
|
+
errorClass: errorClassName(err),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
if (name !== 'register_agent' && isInvalidAgentTokenToolResult(result)) {
|
|
529
|
+
invalidateAgentToken(asIdentity);
|
|
530
|
+
if (toolMetadata) {
|
|
531
|
+
trackAgentRelayToolCall({
|
|
532
|
+
toolName: name,
|
|
533
|
+
toolType: toolMetadata.toolType,
|
|
534
|
+
toolCategory: toolMetadata.toolCategory,
|
|
535
|
+
transport: telemetryTransport,
|
|
536
|
+
startedAt,
|
|
537
|
+
success: false,
|
|
538
|
+
errorClass: 'InvalidAgentToken',
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (hasContentArray(result)) {
|
|
542
|
+
result.content.push({ type: 'text', text: agentTokenRecoveryMessage() });
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
if (!SKIP_PIGGYBACK.has(name) && getSession().agentToken && hasContentArray(result)) {
|
|
547
|
+
try {
|
|
548
|
+
const inbox = await getAgentClient(asIdentity).inbox();
|
|
549
|
+
const inboxText = formatInbox(inbox, asIdentity ?? getSession().agentName);
|
|
550
|
+
if (inboxText) {
|
|
551
|
+
result.content.push({ type: 'text', text: inboxText });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
if (isInvalidAgentTokenError(err)) {
|
|
556
|
+
invalidateAgentToken(asIdentity);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (toolMetadata) {
|
|
561
|
+
const resultIsError = isErrorToolResult(result);
|
|
562
|
+
trackAgentRelayToolCall({
|
|
563
|
+
toolName: name,
|
|
564
|
+
toolType: toolMetadata.toolType,
|
|
565
|
+
toolCategory: toolMetadata.toolCategory,
|
|
566
|
+
transport: telemetryTransport,
|
|
567
|
+
startedAt,
|
|
568
|
+
success: !resultIsError,
|
|
569
|
+
...(resultIsError ? { errorClass: 'ToolResultError' } : {}),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
};
|
|
574
|
+
return original(name, config, wrapped);
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async function createWorkspace(name, baseUrl) {
|
|
578
|
+
return (await RelayCast.createWorkspace(name, {
|
|
579
|
+
baseUrl,
|
|
580
|
+
...relaycastWorkspaceTelemetryOptions(),
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
function extractWorkspaceKey(payload) {
|
|
584
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
|
|
585
|
+
const value = payload.workspaceKey ??
|
|
586
|
+
payload.workspace_key ??
|
|
587
|
+
payload.apiKey ??
|
|
588
|
+
payload.api_key ??
|
|
589
|
+
data.workspaceKey ??
|
|
590
|
+
data.workspace_key ??
|
|
591
|
+
data.apiKey ??
|
|
592
|
+
data.api_key;
|
|
593
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
594
|
+
}
|
|
595
|
+
function extractWorkspaceName(payload, fallback) {
|
|
596
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
|
|
597
|
+
const value = payload.workspaceName ?? payload.workspace_name ?? payload.name ?? data.workspaceName;
|
|
598
|
+
return typeof value === 'string' && value.trim() ? value : fallback;
|
|
599
|
+
}
|
|
600
|
+
function requireWorkspaceKey(session) {
|
|
601
|
+
if (session.workspaceKey) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
throw new Error('Workspace key not configured. Call "create_workspace" first, or "set_workspace_key" if someone shared a workspace key.');
|
|
605
|
+
}
|
|
606
|
+
function jsonContent(value) {
|
|
607
|
+
const structuredContent = typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
608
|
+
? value
|
|
609
|
+
: { value };
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
612
|
+
structuredContent,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function textContent(message, structuredContent = { message }) {
|
|
616
|
+
return {
|
|
617
|
+
content: [{ type: 'text', text: message }],
|
|
618
|
+
structuredContent,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function isSchemaObject(schema) {
|
|
622
|
+
return Boolean(schema &&
|
|
623
|
+
typeof schema === 'object' &&
|
|
624
|
+
!Array.isArray(schema) &&
|
|
625
|
+
typeof schema.safeParse !== 'function');
|
|
626
|
+
}
|
|
627
|
+
function getSchemaDescription(schema) {
|
|
628
|
+
return isSchemaObject(schema) && typeof schema.description === 'string' ? schema.description : undefined;
|
|
629
|
+
}
|
|
630
|
+
function zodFromJsonSchema(schema) {
|
|
631
|
+
if (schema === false) {
|
|
632
|
+
return z.never();
|
|
633
|
+
}
|
|
634
|
+
if (!isSchemaObject(schema)) {
|
|
635
|
+
return z.unknown();
|
|
636
|
+
}
|
|
637
|
+
let zodType;
|
|
638
|
+
const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
639
|
+
switch (schemaType) {
|
|
640
|
+
case 'array':
|
|
641
|
+
zodType = z.array(zodFromJsonSchema(schema.items));
|
|
642
|
+
break;
|
|
643
|
+
case 'boolean':
|
|
644
|
+
zodType = z.boolean();
|
|
645
|
+
break;
|
|
646
|
+
case 'integer':
|
|
647
|
+
zodType = z.number().int();
|
|
648
|
+
break;
|
|
649
|
+
case 'number':
|
|
650
|
+
zodType = z.number();
|
|
651
|
+
break;
|
|
652
|
+
case 'object':
|
|
653
|
+
if (schema.properties) {
|
|
654
|
+
const required = new Set(schema.required ?? []);
|
|
655
|
+
const shape = {};
|
|
656
|
+
for (const [key, childSchema] of Object.entries(schema.properties)) {
|
|
657
|
+
const child = zodFromJsonSchema(childSchema);
|
|
658
|
+
shape[key] = required.has(key) ? child : child.optional();
|
|
659
|
+
}
|
|
660
|
+
zodType = z.object(shape).passthrough();
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
zodType = z.record(z.string(), z.unknown());
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
case 'string':
|
|
667
|
+
zodType = z.string();
|
|
668
|
+
break;
|
|
669
|
+
default:
|
|
670
|
+
zodType = z.unknown();
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
const description = getSchemaDescription(schema);
|
|
674
|
+
return description ? zodType.describe(description) : zodType;
|
|
675
|
+
}
|
|
676
|
+
function actionToolInputSchema(schema) {
|
|
677
|
+
const zodShape = zodObjectShape(schema);
|
|
678
|
+
if (zodShape) {
|
|
679
|
+
return zodShape;
|
|
680
|
+
}
|
|
681
|
+
if (!isSchemaObject(schema) || schema.type !== 'object') {
|
|
682
|
+
return {
|
|
683
|
+
input: z.unknown().describe('Action input payload. The action registry performs final validation.'),
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const required = new Set(schema.required ?? []);
|
|
687
|
+
const shape = {};
|
|
688
|
+
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
689
|
+
const child = zodFromJsonSchema(childSchema);
|
|
690
|
+
shape[key] = required.has(key) ? child : child.optional();
|
|
691
|
+
}
|
|
692
|
+
return shape;
|
|
693
|
+
}
|
|
694
|
+
function actionInvocationInput(descriptor, args) {
|
|
695
|
+
const schema = descriptor.inputSchema;
|
|
696
|
+
if (zodObjectShape(schema)) {
|
|
697
|
+
return args;
|
|
698
|
+
}
|
|
699
|
+
if (!isSchemaObject(schema) || schema.type !== 'object') {
|
|
700
|
+
return typeof args === 'object' && args !== null && 'input' in args
|
|
701
|
+
? args.input
|
|
702
|
+
: args;
|
|
703
|
+
}
|
|
704
|
+
return args;
|
|
705
|
+
}
|
|
706
|
+
function zodObjectShape(schema) {
|
|
707
|
+
if (schema instanceof z.ZodObject) {
|
|
708
|
+
return schema.shape;
|
|
709
|
+
}
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
function serializableActionDescriptor(descriptor) {
|
|
713
|
+
return {
|
|
714
|
+
name: descriptor.name,
|
|
715
|
+
description: descriptor.description,
|
|
716
|
+
visibility: descriptor.visibility,
|
|
717
|
+
...(descriptor.inputSchema ? { inputSchema: serializableActionSchema(descriptor.inputSchema) } : {}),
|
|
718
|
+
...(descriptor.outputSchema ? { outputSchema: serializableActionSchema(descriptor.outputSchema) } : {}),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function serializableActionSchema(schema) {
|
|
722
|
+
if (isSchemaObject(schema)) {
|
|
723
|
+
return schema;
|
|
724
|
+
}
|
|
725
|
+
if (isZodLikeSchema(schema)) {
|
|
726
|
+
return {
|
|
727
|
+
type: 'zod',
|
|
728
|
+
...(schema.description ? { description: schema.description } : {}),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
return schema;
|
|
732
|
+
}
|
|
733
|
+
function isZodLikeSchema(schema) {
|
|
734
|
+
return Boolean(schema &&
|
|
735
|
+
typeof schema === 'object' &&
|
|
736
|
+
!Array.isArray(schema) &&
|
|
737
|
+
typeof schema.safeParse === 'function');
|
|
738
|
+
}
|
|
739
|
+
function registerAgentRelayActionTools(server, actions, getSession, onAuditEvent, getAgentClient, actionToolNames) {
|
|
740
|
+
if (!actions) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Fire-and-forget invocation through the relay: returns an immediate ack
|
|
745
|
+
* (with an `invocation_id`) and does NOT run the handler inline. Falls back to
|
|
746
|
+
* the local in-process registry when the relay action surface is unavailable.
|
|
747
|
+
*/
|
|
748
|
+
const invokeAction = async (name, input) => {
|
|
749
|
+
const relayActions = getRelayAgentActions(getAgentClient);
|
|
750
|
+
if (relayActions) {
|
|
751
|
+
try {
|
|
752
|
+
const ack = await relayActions.invoke(name, asInputRecord(input));
|
|
753
|
+
return jsonContent({ ok: true, status: 'invoked', invocation: ack });
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
return { ...jsonContent({ ok: false, error: errorMessage(error) }), isError: true };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const session = getSession();
|
|
760
|
+
const result = await actions.invoke({
|
|
761
|
+
name,
|
|
762
|
+
input,
|
|
763
|
+
context: {
|
|
764
|
+
caller: { name: session.agentName ?? 'mcp', type: 'agent' },
|
|
765
|
+
emit: onAuditEvent,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
return result.ok ? jsonContent(result) : { ...jsonContent(result), isError: true };
|
|
769
|
+
};
|
|
770
|
+
server.registerTool('list_actions', {
|
|
771
|
+
title: 'List Actions',
|
|
772
|
+
description: 'List Agent Relay actions available to this agent.',
|
|
773
|
+
inputSchema: {},
|
|
774
|
+
outputSchema: jsonResult,
|
|
775
|
+
annotations: {
|
|
776
|
+
readOnlyHint: true,
|
|
777
|
+
destructiveHint: false,
|
|
778
|
+
idempotentHint: true,
|
|
779
|
+
openWorldHint: false,
|
|
780
|
+
},
|
|
781
|
+
}, async () => jsonContent({
|
|
782
|
+
actions: (await actions.list({ visibility: 'agent' })).map(serializableActionDescriptor),
|
|
783
|
+
}));
|
|
784
|
+
server.registerTool('invoke_action', {
|
|
785
|
+
title: 'Invoke Action',
|
|
786
|
+
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.',
|
|
787
|
+
inputSchema: {
|
|
788
|
+
name: z.string().describe('Registered action name'),
|
|
789
|
+
input: z.unknown().describe('Action input payload'),
|
|
790
|
+
},
|
|
791
|
+
outputSchema: jsonResult,
|
|
792
|
+
annotations: {
|
|
793
|
+
readOnlyHint: false,
|
|
794
|
+
destructiveHint: false,
|
|
795
|
+
idempotentHint: false,
|
|
796
|
+
openWorldHint: false,
|
|
797
|
+
},
|
|
798
|
+
}, async ({ name, input }) => invokeAction(name, input));
|
|
799
|
+
void actions
|
|
800
|
+
.list({ visibility: 'agent' })
|
|
801
|
+
.then((descriptors) => {
|
|
802
|
+
for (const descriptor of descriptors) {
|
|
803
|
+
actionToolNames?.add(descriptor.name);
|
|
804
|
+
server.registerTool(descriptor.name, {
|
|
805
|
+
title: descriptor.name,
|
|
806
|
+
description: descriptor.description,
|
|
807
|
+
inputSchema: actionToolInputSchema(descriptor.inputSchema),
|
|
808
|
+
outputSchema: jsonResult,
|
|
809
|
+
annotations: {
|
|
810
|
+
readOnlyHint: false,
|
|
811
|
+
destructiveHint: false,
|
|
812
|
+
idempotentHint: false,
|
|
813
|
+
openWorldHint: false,
|
|
814
|
+
},
|
|
815
|
+
}, async (args) => invokeAction(descriptor.name, actionInvocationInput(descriptor, args)));
|
|
816
|
+
}
|
|
817
|
+
})
|
|
818
|
+
.catch(() => undefined);
|
|
819
|
+
}
|
|
820
|
+
/** The relay-backed action surface on the live agent client, when available. */
|
|
821
|
+
function getRelayAgentActions(getAgentClient) {
|
|
822
|
+
if (!getAgentClient) {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
826
|
+
return getAgentClient().actions;
|
|
703
827
|
}
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
function readAsIdentity(args) {
|
|
707
|
-
const [input] = args;
|
|
708
|
-
if (typeof input !== 'object' || input === null)
|
|
828
|
+
catch {
|
|
709
829
|
return undefined;
|
|
710
|
-
|
|
711
|
-
return typeof as === 'string' ? as : undefined;
|
|
712
|
-
}
|
|
713
|
-
function invalidAgentTokenToolResult() {
|
|
714
|
-
const text = agentTokenRecoveryMessage();
|
|
715
|
-
return {
|
|
716
|
-
content: [{ type: 'text', text }],
|
|
717
|
-
structuredContent: {
|
|
718
|
-
error: { code: INVALID_AGENT_TOKEN_CODE, message: text },
|
|
719
|
-
},
|
|
720
|
-
isError: true,
|
|
721
|
-
};
|
|
830
|
+
}
|
|
722
831
|
}
|
|
723
|
-
|
|
724
|
-
|
|
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') {
|
|
832
|
+
function asInputRecord(input) {
|
|
833
|
+
if (input === undefined || input === null) {
|
|
759
834
|
return undefined;
|
|
760
835
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
return undefined;
|
|
836
|
+
if (typeof input === 'object' && !Array.isArray(input)) {
|
|
837
|
+
return input;
|
|
764
838
|
}
|
|
765
|
-
|
|
766
|
-
return typeof actionName === 'string' && actionName.trim() ? actionName : undefined;
|
|
839
|
+
return { input };
|
|
767
840
|
}
|
|
768
|
-
function
|
|
769
|
-
|
|
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';
|
|
781
|
-
}
|
|
841
|
+
function errorMessage(error) {
|
|
842
|
+
return error instanceof Error ? error.message : String(error);
|
|
782
843
|
}
|
|
783
|
-
function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
844
|
+
function createRegisteredAgent(agentName, agentToken) {
|
|
845
|
+
return { agentName, agentToken };
|
|
846
|
+
}
|
|
847
|
+
export async function registerAgentWithRebind({ session, setSession, getRelay, name, type, persona, metadata, strictAgentName, preferredAgentName, forcedAgentType, }) {
|
|
848
|
+
requireWorkspaceKey(session);
|
|
849
|
+
const configuredName = session.agentName ?? preferredAgentName?.trim() ?? null;
|
|
850
|
+
const warnings = [];
|
|
851
|
+
const effectiveName = strictAgentName && configuredName ? configuredName : name;
|
|
852
|
+
if (strictAgentName && configuredName && name.trim() !== configuredName) {
|
|
853
|
+
warnings.push(`Strict worker identity is enabled; ignoring requested name "${name}" and using "${configuredName}".`);
|
|
790
854
|
}
|
|
791
|
-
const
|
|
792
|
-
if (
|
|
793
|
-
|
|
855
|
+
const effectiveType = forcedAgentType ?? type;
|
|
856
|
+
if (forcedAgentType && type && type !== forcedAgentType) {
|
|
857
|
+
warnings.push(`Forced worker type is enabled; ignoring requested type "${type}" and using "${forcedAgentType}".`);
|
|
794
858
|
}
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
859
|
+
if (session.agentToken && effectiveName && strictAgentName) {
|
|
860
|
+
// If the session tracks per-identity agents, only short-circuit when the
|
|
861
|
+
// strict-named identity is still registered. After an `agent_token_invalid`
|
|
862
|
+
// recovery the entry is dropped from the map, which lets this fall through
|
|
863
|
+
// to a fresh registerOrRotate instead of handing back the dead token.
|
|
864
|
+
const cachedAgent = session.agents?.get(effectiveName);
|
|
865
|
+
const knowsIdentities = session.agents !== undefined;
|
|
866
|
+
if (!knowsIdentities || cachedAgent) {
|
|
867
|
+
return {
|
|
868
|
+
name: effectiveName,
|
|
869
|
+
token: cachedAgent?.agentToken ?? session.agentToken,
|
|
870
|
+
registered_name: effectiveName,
|
|
871
|
+
warnings,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
800
874
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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 } : {}),
|
|
875
|
+
const relay = getRelay();
|
|
876
|
+
const result = await relay.agents.registerOrRotate({
|
|
877
|
+
name: effectiveName,
|
|
878
|
+
type: effectiveType,
|
|
879
|
+
persona,
|
|
880
|
+
metadata,
|
|
815
881
|
});
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return original(name, config, handler);
|
|
823
|
-
}
|
|
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);
|
|
882
|
+
const reboundName = result.name?.trim() ? result.name : effectiveName;
|
|
883
|
+
setSession({ agentToken: result.token, agentName: reboundName });
|
|
884
|
+
return {
|
|
885
|
+
...result,
|
|
886
|
+
registered_name: reboundName,
|
|
887
|
+
warnings,
|
|
906
888
|
};
|
|
907
889
|
}
|
|
908
890
|
function resolveEmoji(input) {
|
|
@@ -1047,6 +1029,23 @@ function registerAgentRelayTools(server, getRelay, getAgentClient, getSession, s
|
|
|
1047
1029
|
const agents = await getRelay().agents.list(status ? { status } : undefined);
|
|
1048
1030
|
return jsonContent({ agents });
|
|
1049
1031
|
});
|
|
1032
|
+
server.registerTool('query_nodes', {
|
|
1033
|
+
title: 'Query Fleet Nodes',
|
|
1034
|
+
description: 'Query registered fleet nodes by capability or name.',
|
|
1035
|
+
inputSchema: {
|
|
1036
|
+
capability: z.string().optional().describe('Optional capability name filter'),
|
|
1037
|
+
name: z.string().optional().describe('Optional node name filter'),
|
|
1038
|
+
},
|
|
1039
|
+
outputSchema: {
|
|
1040
|
+
nodes: z.array(z.object({}).passthrough()).describe('Fleet nodes'),
|
|
1041
|
+
},
|
|
1042
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1043
|
+
}, async ({ capability, name }) => {
|
|
1044
|
+
const session = getSession();
|
|
1045
|
+
requireWorkspaceKey(session);
|
|
1046
|
+
const relay = new AgentRelay({ workspaceKey: session.workspaceKey ?? undefined, baseUrl });
|
|
1047
|
+
return jsonContent({ nodes: await relay.nodes.list({ capability, name }) });
|
|
1048
|
+
});
|
|
1050
1049
|
server.registerTool('create_channel', {
|
|
1051
1050
|
title: 'Create Channel',
|
|
1052
1051
|
description: 'Create a new workspace channel.',
|
|
@@ -1338,27 +1337,94 @@ function registerAgentRelayTools(server, getRelay, getAgentClient, getSession, s
|
|
|
1338
1337
|
}, async ({ message_id, as }) => jsonContent({ readers: await getAgentClient(as).readers(message_id) }));
|
|
1339
1338
|
server.registerTool('add_agent', {
|
|
1340
1339
|
title: 'Add Agent',
|
|
1341
|
-
description: '
|
|
1340
|
+
description: 'Spawn another AI agent (relay worker) to delegate a task to. This is how you ' +
|
|
1341
|
+
'create workers — including non-Claude ones. Use it for any "spawn a <tool> agent" request. ' +
|
|
1342
|
+
'Examples: "spawn a codex agent" → cli:"codex"; ' +
|
|
1343
|
+
'"spawn an opus claude agent" → cli:"claude", model:"claude-opus-4-8"; ' +
|
|
1344
|
+
'"spawn a sonnet claude agent" → cli:"claude", model:"claude-sonnet-4-6". ' +
|
|
1345
|
+
'Do NOT use the built-in Agent/Task tool for relay workers.',
|
|
1342
1346
|
inputSchema: {
|
|
1343
1347
|
name: z.string().describe('Worker agent name'),
|
|
1344
|
-
cli: z
|
|
1348
|
+
cli: z
|
|
1349
|
+
.enum(['claude', 'codex', 'gemini', 'aider', 'goose', 'grok', 'opencode'])
|
|
1350
|
+
.describe('Which AI CLI runs the worker: "codex agent" → codex, "gemini agent" → gemini, ' +
|
|
1351
|
+
'"claude/opus claude/sonnet claude agent" → claude (default).'),
|
|
1345
1352
|
task: z.string().describe('Task instructions'),
|
|
1346
1353
|
channel: z.string().optional().describe('Channel to join'),
|
|
1347
1354
|
persona: z.string().optional().describe('Worker persona'),
|
|
1348
|
-
model: z
|
|
1355
|
+
model: z
|
|
1356
|
+
.string()
|
|
1357
|
+
.optional()
|
|
1358
|
+
.describe('Model to pin (Claude only). Required when a tier is specified: ' +
|
|
1359
|
+
'"opus claude" → claude-opus-4-8, "sonnet claude" → claude-sonnet-4-6, ' +
|
|
1360
|
+
'"haiku" → claude-haiku-4-5-20251001.'),
|
|
1361
|
+
spawn_mode: z
|
|
1362
|
+
.enum(['interactive', 'task_exit', 'task-exit', 'single_shot', 'single-shot'])
|
|
1363
|
+
.optional()
|
|
1364
|
+
.describe('Spawn lifecycle. Use task_exit to exit after the injected task completes.'),
|
|
1365
|
+
exit_after_task: z
|
|
1366
|
+
.boolean()
|
|
1367
|
+
.optional()
|
|
1368
|
+
.describe('Exit the worker after it completes the injected task.'),
|
|
1349
1369
|
},
|
|
1350
1370
|
outputSchema: jsonResult,
|
|
1351
1371
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1352
|
-
}, async ({ name, cli, task, channel, persona, model }) => jsonContent(await getRelay().agents.spawn({
|
|
1372
|
+
}, async ({ name, cli, task, channel, persona, model, spawn_mode, exit_after_task }) => jsonContent(await getRelay().agents.spawn({
|
|
1353
1373
|
name,
|
|
1354
|
-
|
|
1355
|
-
|
|
1374
|
+
// The broker/gateway support grok and opencode at runtime, but the
|
|
1375
|
+
// @relaycast/sdk SpawnAgentRequest type narrows cli to the core five.
|
|
1376
|
+
// Cast to keep grok/opencode selectable from the MCP tool enum.
|
|
1377
|
+
cli: cli,
|
|
1378
|
+
task: exit_after_task ||
|
|
1379
|
+
spawn_mode === 'task_exit' ||
|
|
1380
|
+
spawn_mode === 'task-exit' ||
|
|
1381
|
+
spawn_mode === 'single_shot' ||
|
|
1382
|
+
spawn_mode === 'single-shot'
|
|
1383
|
+
? withExitAfterTaskInstruction(task)
|
|
1384
|
+
: task,
|
|
1356
1385
|
channel,
|
|
1357
1386
|
persona,
|
|
1358
|
-
//
|
|
1359
|
-
// the
|
|
1360
|
-
|
|
1387
|
+
// SpawnAgentRequest has no top-level model field; pass via metadata
|
|
1388
|
+
// so the broker can extract it and forward --model to the launched CLI.
|
|
1389
|
+
metadata: model ? { model } : undefined,
|
|
1361
1390
|
})));
|
|
1391
|
+
server.registerTool('spawn', {
|
|
1392
|
+
title: 'Spawn Agent',
|
|
1393
|
+
description: 'Invoke the fleet spawn action. Optionally target a specific node.',
|
|
1394
|
+
inputSchema: {
|
|
1395
|
+
name: z.string().describe('Agent name'),
|
|
1396
|
+
cli: z.enum(['claude', 'codex', 'gemini', 'aider', 'goose']).describe('AI CLI to launch'),
|
|
1397
|
+
task: z.string().optional().describe('Initial task instructions'),
|
|
1398
|
+
channel: z.string().optional().describe('Channel to join'),
|
|
1399
|
+
channels: z.array(z.string()).optional().describe('Channels to join'),
|
|
1400
|
+
model: z.string().optional().describe('Model powering the worker'),
|
|
1401
|
+
session_ref: z.string().optional().describe('Session reference for resumable spawns'),
|
|
1402
|
+
target_node: z.string().optional().describe('Optional target fleet node name'),
|
|
1403
|
+
...identityOverrideInputShape,
|
|
1404
|
+
},
|
|
1405
|
+
outputSchema: jsonResult,
|
|
1406
|
+
annotations: {
|
|
1407
|
+
readOnlyHint: false,
|
|
1408
|
+
destructiveHint: false,
|
|
1409
|
+
idempotentHint: false,
|
|
1410
|
+
openWorldHint: true,
|
|
1411
|
+
},
|
|
1412
|
+
}, async ({ name, cli, task, channel, channels, model, session_ref, target_node, as }) => {
|
|
1413
|
+
const actions = getAgentClient(as).actions;
|
|
1414
|
+
if (!actions) {
|
|
1415
|
+
throw new Error('spawn requires an agent-scoped Relaycast actions client.');
|
|
1416
|
+
}
|
|
1417
|
+
const actionInput = {
|
|
1418
|
+
name,
|
|
1419
|
+
cli,
|
|
1420
|
+
...(task ? { task } : {}),
|
|
1421
|
+
...(model ? { model } : {}),
|
|
1422
|
+
...(session_ref ? { session_ref } : {}),
|
|
1423
|
+
...(target_node ? { target_node } : {}),
|
|
1424
|
+
...((channels ?? (channel ? [channel] : undefined)) ? { channels: channels ?? [channel] } : {}),
|
|
1425
|
+
};
|
|
1426
|
+
return jsonContent({ invocation: await actions.invoke('spawn', actionInput) });
|
|
1427
|
+
});
|
|
1362
1428
|
server.registerTool('remove_agent', {
|
|
1363
1429
|
title: 'Remove Agent',
|
|
1364
1430
|
description: 'Release a worker agent from active duty.',
|
|
@@ -1430,10 +1496,10 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1430
1496
|
if (session.agentToken && !session.wsBridge && !session.wsInitAttempted) {
|
|
1431
1497
|
try {
|
|
1432
1498
|
const subscriptions = new SubscriptionManager();
|
|
1433
|
-
const wsClient = new WsClient(
|
|
1499
|
+
const wsClient = new WsClient({
|
|
1434
1500
|
token: session.agentToken,
|
|
1435
1501
|
baseUrl: options.baseUrl,
|
|
1436
|
-
})
|
|
1502
|
+
});
|
|
1437
1503
|
const wsBridge = new RealtimeResourceBridge(wsClient, subscriptions, (uri) => {
|
|
1438
1504
|
mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined);
|
|
1439
1505
|
});
|
|
@@ -1457,9 +1523,6 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1457
1523
|
nextAgents.delete(targetName);
|
|
1458
1524
|
partial.agents = nextAgents;
|
|
1459
1525
|
}
|
|
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
1526
|
if (!asIdentity || asIdentity === session.agentName) {
|
|
1464
1527
|
if (session.agentToken !== null) {
|
|
1465
1528
|
partial.agentToken = null;
|
|
@@ -1536,9 +1599,6 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1536
1599
|
return result;
|
|
1537
1600
|
});
|
|
1538
1601
|
}
|
|
1539
|
-
if (session.agentToken && !session.wsBridge) {
|
|
1540
|
-
setSession({ agentToken: session.agentToken, agentName: session.agentName });
|
|
1541
|
-
}
|
|
1542
1602
|
return mcpServer;
|
|
1543
1603
|
}
|
|
1544
1604
|
/** Relaycast agent tokens are opaque `at_live_<hex>` literals. Anything else
|