agent-relay 3.2.22 → 4.0.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.
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +4989 -1481
- package/dist/src/cli/bootstrap.d.ts.map +1 -1
- package/dist/src/cli/bootstrap.js +2 -0
- package/dist/src/cli/bootstrap.js.map +1 -1
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +14 -4
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts +2 -6
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +30 -12
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +10 -3
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/commands/monitoring.d.ts +2 -2
- package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
- package/dist/src/cli/commands/monitoring.js +15 -6
- package/dist/src/cli/commands/monitoring.js.map +1 -1
- package/dist/src/cli/commands/on/dotfiles.d.ts +35 -0
- package/dist/src/cli/commands/on/dotfiles.d.ts.map +1 -0
- package/dist/src/cli/commands/on/dotfiles.js +157 -0
- package/dist/src/cli/commands/on/dotfiles.js.map +1 -0
- package/dist/src/cli/commands/on/prereqs.d.ts +15 -0
- package/dist/src/cli/commands/on/prereqs.d.ts.map +1 -0
- package/dist/src/cli/commands/on/prereqs.js +103 -0
- package/dist/src/cli/commands/on/prereqs.js.map +1 -0
- package/dist/src/cli/commands/on/provision.d.ts +22 -0
- package/dist/src/cli/commands/on/provision.d.ts.map +1 -0
- package/dist/src/cli/commands/on/provision.js +157 -0
- package/dist/src/cli/commands/on/provision.js.map +1 -0
- package/dist/src/cli/commands/on/scan.d.ts +8 -0
- package/dist/src/cli/commands/on/scan.d.ts.map +1 -0
- package/dist/src/cli/commands/on/scan.js +59 -0
- package/dist/src/cli/commands/on/scan.js.map +1 -0
- package/dist/src/cli/commands/on/services.d.ts +17 -0
- package/dist/src/cli/commands/on/services.d.ts.map +1 -0
- package/dist/src/cli/commands/on/services.js +328 -0
- package/dist/src/cli/commands/on/services.js.map +1 -0
- package/dist/src/cli/commands/on/start.d.ts +61 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -0
- package/dist/src/cli/commands/on/start.js +1071 -0
- package/dist/src/cli/commands/on/start.js.map +1 -0
- package/dist/src/cli/commands/on/stop.d.ts +4 -0
- package/dist/src/cli/commands/on/stop.d.ts.map +1 -0
- package/dist/src/cli/commands/on/stop.js +11 -0
- package/dist/src/cli/commands/on/stop.js.map +1 -0
- package/dist/src/cli/commands/on/token.d.ts +8 -0
- package/dist/src/cli/commands/on/token.d.ts.map +1 -0
- package/dist/src/cli/commands/on/token.js +26 -0
- package/dist/src/cli/commands/on/token.js.map +1 -0
- package/dist/src/cli/commands/on/workspace.d.ts +4 -0
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -0
- package/dist/src/cli/commands/on/workspace.js +241 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -0
- package/dist/src/cli/commands/on.d.ts +10 -0
- package/dist/src/cli/commands/on.d.ts.map +1 -0
- package/dist/src/cli/commands/on.js +52 -0
- package/dist/src/cli/commands/on.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +10 -21
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/lib/bridge.js +1 -1
- package/dist/src/cli/lib/bridge.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts +14 -4
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +82 -120
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +2 -2
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.js +14 -11
- package/dist/src/cli/lib/client-factory.js.map +1 -1
- package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
- package/dist/src/cli/lib/core-maintenance.js +11 -22
- package/dist/src/cli/lib/core-maintenance.js.map +1 -1
- package/package.json +14 -11
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +10 -3
- package/packages/sdk/dist/client.d.ts +108 -196
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +336 -824
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/examples/example.js +2 -5
- package/packages/sdk/dist/examples/example.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +3 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +3 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay-adapter.d.ts +9 -26
- package/packages/sdk/dist/relay-adapter.d.ts.map +1 -1
- package/packages/sdk/dist/relay-adapter.js +75 -47
- package/packages/sdk/dist/relay-adapter.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +24 -5
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +213 -43
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/transport.d.ts +58 -0
- package/packages/sdk/dist/transport.d.ts.map +1 -0
- package/packages/sdk/dist/transport.js +184 -0
- package/packages/sdk/dist/transport.js.map +1 -0
- package/packages/sdk/dist/types.d.ts +69 -0
- package/packages/sdk/dist/types.d.ts.map +1 -0
- package/packages/sdk/dist/types.js +5 -0
- package/packages/sdk/dist/types.js.map +1 -0
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +17 -2
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/unit.test.ts +100 -1
- package/packages/sdk/src/client.ts +422 -1072
- package/packages/sdk/src/examples/example.ts +2 -5
- package/packages/sdk/src/index.ts +8 -1
- package/packages/sdk/src/relay-adapter.ts +75 -55
- package/packages/sdk/src/relay.ts +260 -57
- package/packages/sdk/src/transport.ts +216 -0
- package/packages/sdk/src/types.ts +75 -0
- package/packages/sdk/src/workflows/validator.ts +20 -2
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +0 -8
- package/packages/sdk-py/src/agent_relay/client.py +329 -522
- package/packages/sdk-py/src/agent_relay/protocol.py +2 -96
- package/packages/sdk-py/src/agent_relay/relay.py +1 -4
- package/packages/sdk-py/tests/test_wait_for_api_url.py +92 -0
- package/packages/sdk-py/uv.lock +5388 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -1
- package/packages/telemetry/dist/client.js +1 -1
- package/packages/telemetry/dist/client.js.map +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +3 -10
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/scripts/postinstall.js +121 -1
|
@@ -24,15 +24,14 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { randomBytes } from 'node:crypto';
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
28
|
import path from 'node:path';
|
|
28
29
|
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
type SpawnPtyInput,
|
|
35
|
-
} from './client.js';
|
|
30
|
+
import { RelayCast } from '@relaycast/sdk';
|
|
31
|
+
|
|
32
|
+
import { AgentRelayClient, type AgentRelaySpawnOptions } from './client.js';
|
|
33
|
+
import { AgentRelayProtocolError } from './transport.js';
|
|
34
|
+
import type { SendMessageInput, SpawnPtyInput } from './types.js';
|
|
36
35
|
import type {
|
|
37
36
|
AgentRuntime,
|
|
38
37
|
BrokerEvent,
|
|
@@ -75,6 +74,71 @@ function buildUnsupportedOperationMessage(
|
|
|
75
74
|
};
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
interface WorkspaceRegistryEntry {
|
|
78
|
+
relaycastApiKey?: string;
|
|
79
|
+
relayfileUrl?: string;
|
|
80
|
+
createdAt?: string;
|
|
81
|
+
agents?: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type WorkspaceRegistry = Record<string, WorkspaceRegistryEntry>;
|
|
85
|
+
|
|
86
|
+
const WORKSPACE_ID_PREFIX = 'rw_';
|
|
87
|
+
const WORKSPACE_ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
88
|
+
|
|
89
|
+
function normalizeWorkspaceId(value: string | undefined): string | undefined {
|
|
90
|
+
const trimmed = value?.trim();
|
|
91
|
+
return trimmed ? trimmed : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function generateWorkspaceId(): string {
|
|
95
|
+
const alphabetLength = WORKSPACE_ID_ALPHABET.length;
|
|
96
|
+
const maxUnbiasedValue = Math.floor(256 / alphabetLength) * alphabetLength;
|
|
97
|
+
let suffix = '';
|
|
98
|
+
|
|
99
|
+
while (suffix.length < 8) {
|
|
100
|
+
const bytes = randomBytes(8 - suffix.length);
|
|
101
|
+
for (const byte of bytes) {
|
|
102
|
+
if (byte >= maxUnbiasedValue) continue;
|
|
103
|
+
suffix += WORKSPACE_ID_ALPHABET[byte % alphabetLength];
|
|
104
|
+
if (suffix.length === 8) break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return `${WORKSPACE_ID_PREFIX}${suffix}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toWorkspaceRegistryEntry(value: unknown): WorkspaceRegistryEntry {
|
|
112
|
+
if (!value || typeof value !== 'object') {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const record = value as Record<string, unknown>;
|
|
117
|
+
const relaycastApiKey =
|
|
118
|
+
typeof record.relaycastApiKey === 'string' && record.relaycastApiKey.trim()
|
|
119
|
+
? record.relaycastApiKey.trim()
|
|
120
|
+
: undefined;
|
|
121
|
+
const relayfileUrl =
|
|
122
|
+
typeof record.relayfileUrl === 'string' && record.relayfileUrl.trim()
|
|
123
|
+
? record.relayfileUrl.trim()
|
|
124
|
+
: undefined;
|
|
125
|
+
const createdAt =
|
|
126
|
+
typeof record.createdAt === 'string' && record.createdAt.trim() ? record.createdAt.trim() : undefined;
|
|
127
|
+
const agents = Array.isArray(record.agents)
|
|
128
|
+
? record.agents
|
|
129
|
+
.filter((agent): agent is string => typeof agent === 'string')
|
|
130
|
+
.map((agent) => agent.trim())
|
|
131
|
+
.filter((agent) => agent.length > 0)
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...(relaycastApiKey ? { relaycastApiKey } : {}),
|
|
136
|
+
...(relayfileUrl ? { relayfileUrl } : {}),
|
|
137
|
+
...(createdAt ? { createdAt } : {}),
|
|
138
|
+
...(agents && agents.length > 0 ? { agents } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
78
142
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
79
143
|
|
|
80
144
|
export interface Message {
|
|
@@ -201,7 +265,10 @@ export interface Agent {
|
|
|
201
265
|
* @param options.stream — if provided, only invoke callback when the event stream matches (e.g. 'stdout', 'stderr')
|
|
202
266
|
* @param options.mode — 'chunk' for raw string callbacks, 'structured' for { stream, chunk } callbacks. Auto-detected if omitted.
|
|
203
267
|
*/
|
|
204
|
-
onOutput(
|
|
268
|
+
onOutput(
|
|
269
|
+
callback: AgentOutputCallback,
|
|
270
|
+
options?: { stream?: string; mode?: 'chunk' | 'structured' }
|
|
271
|
+
): () => void;
|
|
205
272
|
}
|
|
206
273
|
|
|
207
274
|
export interface HumanHandle {
|
|
@@ -242,11 +309,18 @@ export interface AgentRelayOptions {
|
|
|
242
309
|
cwd?: string;
|
|
243
310
|
env?: NodeJS.ProcessEnv;
|
|
244
311
|
requestTimeoutMs?: number;
|
|
245
|
-
shutdownTimeoutMs?: number;
|
|
246
312
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
313
|
+
* Unified workspace ID shared across relayfile, relayauth claims, and
|
|
314
|
+
* relaycast key lookup.
|
|
315
|
+
*/
|
|
316
|
+
workspaceId?: string;
|
|
317
|
+
/**
|
|
318
|
+
* Display name for an auto-created Relaycast workspace.
|
|
319
|
+
* If omitted, the unified workspace ID is used.
|
|
320
|
+
*
|
|
321
|
+
* @deprecated Since v1.x this field falls back to workspaceId when omitted,
|
|
322
|
+
* changing prior behavior where it was required for workspace naming.
|
|
323
|
+
* Callers relying on distinct naming should set this explicitly.
|
|
250
324
|
*/
|
|
251
325
|
workspaceName?: string;
|
|
252
326
|
/**
|
|
@@ -302,14 +376,17 @@ export class AgentRelay {
|
|
|
302
376
|
readonly gemini: AgentSpawner;
|
|
303
377
|
readonly opencode: AgentSpawner;
|
|
304
378
|
|
|
305
|
-
private readonly clientOptions:
|
|
379
|
+
private readonly clientOptions: AgentRelaySpawnOptions;
|
|
306
380
|
private readonly defaultChannels: string[];
|
|
381
|
+
private readonly requestedWorkspaceId?: string;
|
|
307
382
|
private readonly workspaceName?: string;
|
|
308
383
|
private readonly relaycastBaseUrl?: string;
|
|
309
384
|
private relayApiKey?: string;
|
|
385
|
+
private resolvedWorkspaceId?: string;
|
|
310
386
|
private client?: AgentRelayClient;
|
|
311
387
|
private startPromise?: Promise<AgentRelayClient>;
|
|
312
388
|
private unsubEvent?: () => void;
|
|
389
|
+
private readonly stderrListeners = new Set<(line: string) => void>();
|
|
313
390
|
private readonly knownAgents = new Map<string, Agent>();
|
|
314
391
|
private readonly readyAgents = new Set<string>();
|
|
315
392
|
private readonly messageReadyAgents = new Set<string>();
|
|
@@ -329,18 +406,25 @@ export class AgentRelay {
|
|
|
329
406
|
private idleResolverSeq = 0;
|
|
330
407
|
|
|
331
408
|
constructor(options: AgentRelayOptions = {}) {
|
|
409
|
+
const requestedWorkspaceId = normalizeWorkspaceId(options.workspaceId);
|
|
332
410
|
this.defaultChannels = options.channels ?? ['general'];
|
|
411
|
+
this.requestedWorkspaceId = requestedWorkspaceId;
|
|
333
412
|
this.workspaceName = options.workspaceName;
|
|
413
|
+
if (options.workspaceName && !options.workspaceId) {
|
|
414
|
+
console.warn(
|
|
415
|
+
'[AgentRelay] workspaceName without workspaceId is deprecated and will be removed in a future major version. ' +
|
|
416
|
+
'Set workspaceId explicitly to avoid silent behavior changes.'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
334
419
|
this.relaycastBaseUrl = options.relaycastBaseUrl;
|
|
335
420
|
this.clientOptions = {
|
|
336
421
|
binaryPath: options.binaryPath,
|
|
337
422
|
binaryArgs: options.binaryArgs,
|
|
338
|
-
brokerName: options.brokerName ?? options.workspaceName,
|
|
423
|
+
brokerName: options.brokerName ?? options.workspaceName ?? requestedWorkspaceId,
|
|
339
424
|
channels: this.defaultChannels,
|
|
340
425
|
cwd: options.cwd,
|
|
341
426
|
env: options.env,
|
|
342
427
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
343
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
344
428
|
};
|
|
345
429
|
|
|
346
430
|
this.codex = this.createSpawner('codex', 'Codex', 'pty');
|
|
@@ -349,28 +433,122 @@ export class AgentRelay {
|
|
|
349
433
|
this.opencode = this.createSpawner('opencode', 'OpenCode', 'headless');
|
|
350
434
|
}
|
|
351
435
|
|
|
436
|
+
private getWorkspaceRegistryPath(): string {
|
|
437
|
+
return path.join(this.clientOptions.cwd ?? process.cwd(), '.relay', 'workspaces.json');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private readWorkspaceRegistry(): WorkspaceRegistry {
|
|
441
|
+
const registryPath = this.getWorkspaceRegistryPath();
|
|
442
|
+
if (!existsSync(registryPath)) {
|
|
443
|
+
return {};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let raw: string;
|
|
447
|
+
try {
|
|
448
|
+
raw = readFileSync(registryPath, 'utf8').trim();
|
|
449
|
+
} catch {
|
|
450
|
+
return {};
|
|
451
|
+
}
|
|
452
|
+
if (!raw) {
|
|
453
|
+
return {};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let parsed: unknown;
|
|
457
|
+
try {
|
|
458
|
+
parsed = JSON.parse(raw);
|
|
459
|
+
} catch {
|
|
460
|
+
// Registry file is corrupted (partial write, disk full, concurrent access).
|
|
461
|
+
// Return empty registry so callers can re-create it.
|
|
462
|
+
return {};
|
|
463
|
+
}
|
|
464
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const registry: WorkspaceRegistry = {};
|
|
469
|
+
for (const [workspaceId, entry] of Object.entries(parsed as Record<string, unknown>)) {
|
|
470
|
+
const normalizedId = normalizeWorkspaceId(workspaceId);
|
|
471
|
+
if (!normalizedId) continue;
|
|
472
|
+
registry[normalizedId] = toWorkspaceRegistryEntry(entry);
|
|
473
|
+
}
|
|
474
|
+
return registry;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private writeWorkspaceRegistry(registry: WorkspaceRegistry): void {
|
|
478
|
+
const registryPath = this.getWorkspaceRegistryPath();
|
|
479
|
+
mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
480
|
+
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private persistWorkspaceMapping(workspaceId: string, apiKey: string): void {
|
|
484
|
+
const registry = this.readWorkspaceRegistry();
|
|
485
|
+
const existing = registry[workspaceId] ?? {};
|
|
486
|
+
registry[workspaceId] = {
|
|
487
|
+
...existing,
|
|
488
|
+
relaycastApiKey: apiKey,
|
|
489
|
+
relayfileUrl: existing.relayfileUrl,
|
|
490
|
+
createdAt: existing.createdAt ?? new Date().toISOString(),
|
|
491
|
+
agents: existing.agents ?? [],
|
|
492
|
+
};
|
|
493
|
+
this.writeWorkspaceRegistry(registry);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private findMappedWorkspaceIdByApiKey(apiKey: string): string | undefined {
|
|
497
|
+
const registry = this.readWorkspaceRegistry();
|
|
498
|
+
for (const [workspaceId, entry] of Object.entries(registry)) {
|
|
499
|
+
if (entry.relaycastApiKey === apiKey) {
|
|
500
|
+
return workspaceId;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private getResolvedWorkspaceId(): string | undefined {
|
|
507
|
+
return this.resolvedWorkspaceId ?? this.requestedWorkspaceId;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private getRelaycastBaseUrl(): string {
|
|
511
|
+
return (
|
|
512
|
+
this.relaycastBaseUrl ??
|
|
513
|
+
this.clientOptions.env?.RELAYCAST_BASE_URL ??
|
|
514
|
+
process.env.RELAYCAST_BASE_URL ??
|
|
515
|
+
'https://api.relaycast.dev'
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private applyWorkspaceEnv(workspaceId: string, apiKey: string): void {
|
|
520
|
+
const env: NodeJS.ProcessEnv = {
|
|
521
|
+
...(this.clientOptions.env ?? process.env),
|
|
522
|
+
RELAY_API_KEY: apiKey,
|
|
523
|
+
RELAYFILE_WORKSPACE: workspaceId,
|
|
524
|
+
RELAY_DEFAULT_WORKSPACE: workspaceId,
|
|
525
|
+
RELAY_WORKSPACE_ID: workspaceId,
|
|
526
|
+
RELAY_WORKSPACES_JSON: JSON.stringify([{ workspace_id: workspaceId, api_key: apiKey }]),
|
|
527
|
+
};
|
|
528
|
+
if (this.relaycastBaseUrl) {
|
|
529
|
+
env.RELAYCAST_BASE_URL = this.relaycastBaseUrl;
|
|
530
|
+
}
|
|
531
|
+
this.clientOptions.env = env;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private async createMappedRelaycastWorkspace(workspaceId: string): Promise<string> {
|
|
535
|
+
const created = await RelayCast.createWorkspace(
|
|
536
|
+
this.workspaceName ?? workspaceId,
|
|
537
|
+
this.getRelaycastBaseUrl()
|
|
538
|
+
);
|
|
539
|
+
return created.apiKey;
|
|
540
|
+
}
|
|
541
|
+
|
|
352
542
|
/**
|
|
353
543
|
* Subscribe to broker stderr output. Listener is wired immediately if the
|
|
354
544
|
* client is already started, otherwise it is attached when the client starts.
|
|
355
545
|
* Returns an unsubscribe function.
|
|
356
546
|
*/
|
|
357
547
|
onBrokerStderr(listener: (line: string) => void): () => void {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// Queue it: once ensureStarted completes, wire it up
|
|
362
|
-
let unsub: (() => void) | undefined;
|
|
363
|
-
const queuedUnsub = () => {
|
|
364
|
-
unsub?.();
|
|
548
|
+
this.stderrListeners.add(listener);
|
|
549
|
+
return () => {
|
|
550
|
+
this.stderrListeners.delete(listener);
|
|
365
551
|
};
|
|
366
|
-
// Use the start promise if one is pending
|
|
367
|
-
const promise = this.startPromise ?? this.ensureStarted();
|
|
368
|
-
promise
|
|
369
|
-
.then((c) => {
|
|
370
|
-
unsub = c.onBrokerStderr(listener);
|
|
371
|
-
})
|
|
372
|
-
.catch(() => {});
|
|
373
|
-
return queuedUnsub;
|
|
374
552
|
}
|
|
375
553
|
|
|
376
554
|
// ── Spawning ────────────────────────────────────────────────────────────
|
|
@@ -594,7 +772,7 @@ export class AgentRelay {
|
|
|
594
772
|
/** Pre-register a batch of agents with Relaycast before steps execute. */
|
|
595
773
|
async preflightAgents(agents: Array<{ name: string; cli: string }>): Promise<void> {
|
|
596
774
|
const client = await this.ensureStarted();
|
|
597
|
-
await client.
|
|
775
|
+
await client.preflight(agents);
|
|
598
776
|
}
|
|
599
777
|
|
|
600
778
|
/** List agents with PIDs from the broker (for worker registration). */
|
|
@@ -939,36 +1117,39 @@ export class AgentRelay {
|
|
|
939
1117
|
*/
|
|
940
1118
|
private async ensureRelaycastApiKey(): Promise<void> {
|
|
941
1119
|
if (this.relayApiKey) {
|
|
942
|
-
this.
|
|
1120
|
+
const workspaceId = this.getResolvedWorkspaceId();
|
|
1121
|
+
if (workspaceId) {
|
|
1122
|
+
this.applyWorkspaceEnv(workspaceId, this.relayApiKey);
|
|
1123
|
+
try { this.persistWorkspaceMapping(workspaceId, this.relayApiKey); } catch { /* non-critical */ }
|
|
1124
|
+
} else {
|
|
1125
|
+
this.wireRelaycastBaseUrl();
|
|
1126
|
+
}
|
|
943
1127
|
return;
|
|
944
1128
|
}
|
|
945
1129
|
|
|
946
1130
|
const envKey = this.clientOptions.env?.RELAY_API_KEY ?? process.env.RELAY_API_KEY;
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
this.clientOptions.env.RELAY_API_KEY = envKey;
|
|
958
|
-
}
|
|
959
|
-
this.wireRelaycastBaseUrl();
|
|
1131
|
+
const requestedWorkspaceId = this.requestedWorkspaceId;
|
|
1132
|
+
if (requestedWorkspaceId) {
|
|
1133
|
+
const registry = this.readWorkspaceRegistry();
|
|
1134
|
+
const mappedKey = registry[requestedWorkspaceId]?.relaycastApiKey;
|
|
1135
|
+
const resolvedKey =
|
|
1136
|
+
mappedKey ?? envKey ?? (await this.createMappedRelaycastWorkspace(requestedWorkspaceId));
|
|
1137
|
+
this.relayApiKey = resolvedKey;
|
|
1138
|
+
this.resolvedWorkspaceId = requestedWorkspaceId;
|
|
1139
|
+
this.applyWorkspaceEnv(requestedWorkspaceId, resolvedKey);
|
|
1140
|
+
try { this.persistWorkspaceMapping(requestedWorkspaceId, resolvedKey); } catch { /* non-critical */ }
|
|
960
1141
|
return;
|
|
961
1142
|
}
|
|
962
1143
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (!this.clientOptions.env) {
|
|
968
|
-
this.clientOptions.env = { ...process.env };
|
|
969
|
-
}
|
|
1144
|
+
const resolvedWorkspaceId = envKey
|
|
1145
|
+
? (this.findMappedWorkspaceIdByApiKey(envKey) ?? generateWorkspaceId())
|
|
1146
|
+
: generateWorkspaceId();
|
|
1147
|
+
const resolvedKey = envKey ?? (await this.createMappedRelaycastWorkspace(resolvedWorkspaceId));
|
|
970
1148
|
|
|
971
|
-
this.
|
|
1149
|
+
this.relayApiKey = resolvedKey;
|
|
1150
|
+
this.resolvedWorkspaceId = resolvedWorkspaceId;
|
|
1151
|
+
this.applyWorkspaceEnv(resolvedWorkspaceId, resolvedKey);
|
|
1152
|
+
try { this.persistWorkspaceMapping(resolvedWorkspaceId, resolvedKey); } catch { /* non-critical */ }
|
|
972
1153
|
}
|
|
973
1154
|
|
|
974
1155
|
/** Inject relaycastBaseUrl into broker env. Explicit option wins over inherited env. */
|
|
@@ -983,19 +1164,38 @@ export class AgentRelay {
|
|
|
983
1164
|
if (this.startPromise) return this.startPromise;
|
|
984
1165
|
|
|
985
1166
|
this.startPromise = this.ensureRelaycastApiKey()
|
|
986
|
-
.then(() =>
|
|
1167
|
+
.then(() =>
|
|
1168
|
+
AgentRelayClient.spawn({
|
|
1169
|
+
...this.clientOptions,
|
|
1170
|
+
onStderr: (line) => {
|
|
1171
|
+
for (const listener of this.stderrListeners) {
|
|
1172
|
+
try {
|
|
1173
|
+
listener(line);
|
|
1174
|
+
} catch {
|
|
1175
|
+
/* ignore */
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
})
|
|
1180
|
+
)
|
|
987
1181
|
.then((c) => {
|
|
988
|
-
this.client = c;
|
|
989
|
-
this.startPromise = undefined;
|
|
990
1182
|
// Use the workspace key the broker actually connected with.
|
|
991
1183
|
// This ensures SDK and workers are always on the same workspace.
|
|
992
1184
|
if (c.workspaceKey) {
|
|
993
1185
|
this.relayApiKey = c.workspaceKey;
|
|
1186
|
+
const workspaceId = this.getResolvedWorkspaceId();
|
|
1187
|
+
if (workspaceId) {
|
|
1188
|
+
this.applyWorkspaceEnv(workspaceId, c.workspaceKey);
|
|
1189
|
+
try { this.persistWorkspaceMapping(workspaceId, c.workspaceKey); } catch { /* non-critical */ }
|
|
1190
|
+
}
|
|
994
1191
|
}
|
|
995
1192
|
this.wireEvents(c);
|
|
1193
|
+
this.client = c;
|
|
1194
|
+
this.startPromise = undefined;
|
|
996
1195
|
return c;
|
|
997
1196
|
})
|
|
998
1197
|
.catch((err) => {
|
|
1198
|
+
this.client = undefined;
|
|
999
1199
|
this.startPromise = undefined;
|
|
1000
1200
|
throw err;
|
|
1001
1201
|
});
|
|
@@ -1332,7 +1532,10 @@ export class AgentRelay {
|
|
|
1332
1532
|
async unsubscribe(channelsToRemove: string[]) {
|
|
1333
1533
|
await relay.unsubscribe({ agent: name, channels: channelsToRemove });
|
|
1334
1534
|
},
|
|
1335
|
-
onOutput(
|
|
1535
|
+
onOutput(
|
|
1536
|
+
callback: AgentOutputCallback,
|
|
1537
|
+
options?: { stream?: string; mode?: 'chunk' | 'structured' }
|
|
1538
|
+
): () => void {
|
|
1336
1539
|
let listeners = relay.outputListeners.get(name);
|
|
1337
1540
|
if (!listeners) {
|
|
1338
1541
|
listeners = new Set();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrokerTransport — HTTP/WS transport layer for communicating with the
|
|
3
|
+
* agent-relay broker. Used internally by AgentRelayClient.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - HTTP requests with API key auth and structured error parsing
|
|
7
|
+
* - WebSocket connection for real-time event streaming
|
|
8
|
+
* - Event buffering, replay, and query (mirrors stdio client behavior)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import WebSocket from 'ws';
|
|
12
|
+
import type { BrokerEvent } from './protocol.js';
|
|
13
|
+
|
|
14
|
+
export class AgentRelayProtocolError extends Error {
|
|
15
|
+
code: string;
|
|
16
|
+
retryable: boolean;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
|
|
19
|
+
constructor(payload: { code: string; message: string; retryable?: boolean; data?: unknown }) {
|
|
20
|
+
super(payload.message);
|
|
21
|
+
this.name = 'AgentRelayProtocolError';
|
|
22
|
+
this.code = payload.code;
|
|
23
|
+
this.retryable = payload.retryable ?? false;
|
|
24
|
+
this.data = payload.data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BrokerTransportOptions {
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
/** Timeout in ms for HTTP requests. Default: 30000. */
|
|
32
|
+
requestTimeoutMs?: number;
|
|
33
|
+
/** Maximum number of events to buffer in memory for queryEvents/getLastEvent */
|
|
34
|
+
maxBufferSize?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class BrokerTransport {
|
|
38
|
+
private readonly baseUrl: string;
|
|
39
|
+
private readonly apiKey?: string;
|
|
40
|
+
private readonly requestTimeoutMs: number;
|
|
41
|
+
private readonly maxBufferSize: number;
|
|
42
|
+
|
|
43
|
+
private ws: WebSocket | null = null;
|
|
44
|
+
private eventListeners = new Set<(event: BrokerEvent) => void>();
|
|
45
|
+
private eventBuffer: BrokerEvent[] = [];
|
|
46
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
private sinceSeq = 0;
|
|
48
|
+
private _connected = false;
|
|
49
|
+
private _intentionalClose = false;
|
|
50
|
+
|
|
51
|
+
constructor(options: BrokerTransportOptions) {
|
|
52
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
53
|
+
this.apiKey = options.apiKey;
|
|
54
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
55
|
+
this.maxBufferSize = options.maxBufferSize ?? 1000;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get connected(): boolean {
|
|
59
|
+
return this._connected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get wsUrl(): string {
|
|
63
|
+
return this.baseUrl.replace(/^http/, 'ws') + '/ws';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── HTTP ─────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
|
69
|
+
const headers = new Headers(init?.headers);
|
|
70
|
+
if (!headers.has('Content-Type')) {
|
|
71
|
+
headers.set('Content-Type', 'application/json');
|
|
72
|
+
}
|
|
73
|
+
if (this.apiKey) {
|
|
74
|
+
headers.set('X-API-Key', this.apiKey);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const signal = init?.signal ?? AbortSignal.timeout(this.requestTimeoutMs);
|
|
78
|
+
const res = await fetch(`${this.baseUrl}${path}`, { ...init, headers, signal });
|
|
79
|
+
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
let body: { code?: string; message?: string; error?: string } | undefined;
|
|
82
|
+
try {
|
|
83
|
+
body = (await res.json()) as { code?: string; message?: string; error?: string };
|
|
84
|
+
} catch {
|
|
85
|
+
// non-JSON error
|
|
86
|
+
}
|
|
87
|
+
throw new AgentRelayProtocolError({
|
|
88
|
+
code: body?.code ?? `http_${res.status}`,
|
|
89
|
+
message: body?.message ?? body?.error ?? res.statusText,
|
|
90
|
+
retryable: res.status >= 500,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return res.json() as Promise<T>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── WebSocket events ─────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
connect(sinceSeq?: number): void {
|
|
100
|
+
if (this.ws) return;
|
|
101
|
+
// Clear any pending reconnect timer to avoid duplicate connections
|
|
102
|
+
if (this.reconnectTimer) {
|
|
103
|
+
clearTimeout(this.reconnectTimer);
|
|
104
|
+
this.reconnectTimer = null;
|
|
105
|
+
}
|
|
106
|
+
this.sinceSeq = sinceSeq ?? this.sinceSeq;
|
|
107
|
+
this._connect();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private _connect(): void {
|
|
111
|
+
this._intentionalClose = false;
|
|
112
|
+
const url = `${this.wsUrl}?sinceSeq=${this.sinceSeq}`;
|
|
113
|
+
const headers: Record<string, string> = {};
|
|
114
|
+
if (this.apiKey) {
|
|
115
|
+
headers['X-API-Key'] = this.apiKey;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.ws = new WebSocket(url, { headers });
|
|
119
|
+
|
|
120
|
+
this.ws.on('open', () => {
|
|
121
|
+
this._connected = true;
|
|
122
|
+
if (this.reconnectTimer) {
|
|
123
|
+
clearTimeout(this.reconnectTimer);
|
|
124
|
+
this.reconnectTimer = null;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
this.ws.on('message', (data) => {
|
|
129
|
+
try {
|
|
130
|
+
const event = JSON.parse(data.toString()) as BrokerEvent & { seq?: number };
|
|
131
|
+
// Track sequence for replay on reconnect
|
|
132
|
+
if (typeof event.seq === 'number' && event.seq > this.sinceSeq) {
|
|
133
|
+
this.sinceSeq = event.seq;
|
|
134
|
+
}
|
|
135
|
+
// Buffer the event
|
|
136
|
+
this.eventBuffer.push(event);
|
|
137
|
+
if (this.eventBuffer.length > this.maxBufferSize) {
|
|
138
|
+
this.eventBuffer = this.eventBuffer.slice(-this.maxBufferSize);
|
|
139
|
+
}
|
|
140
|
+
// Notify listeners
|
|
141
|
+
for (const listener of this.eventListeners) {
|
|
142
|
+
try {
|
|
143
|
+
listener(event);
|
|
144
|
+
} catch {
|
|
145
|
+
// don't let a bad listener break the stream
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// ignore non-JSON frames (ping/pong)
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.ws.on('close', () => {
|
|
154
|
+
this._connected = false;
|
|
155
|
+
this.ws = null;
|
|
156
|
+
// Auto-reconnect after 2s unless intentionally closed
|
|
157
|
+
if (!this._intentionalClose) {
|
|
158
|
+
this.reconnectTimer = setTimeout(() => this._connect(), 2000);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.ws.on('error', () => {
|
|
163
|
+
// error always followed by close
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
disconnect(): void {
|
|
168
|
+
this._intentionalClose = true;
|
|
169
|
+
if (this.reconnectTimer) {
|
|
170
|
+
clearTimeout(this.reconnectTimer);
|
|
171
|
+
this.reconnectTimer = null;
|
|
172
|
+
}
|
|
173
|
+
if (this.ws) {
|
|
174
|
+
this.ws.close();
|
|
175
|
+
this.ws = null;
|
|
176
|
+
}
|
|
177
|
+
this._connected = false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onEvent(listener: (event: BrokerEvent) => void): () => void {
|
|
181
|
+
this.eventListeners.add(listener);
|
|
182
|
+
return () => {
|
|
183
|
+
this.eventListeners.delete(listener);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
queryEvents(filter?: { kind?: string; name?: string; since?: number; limit?: number }): BrokerEvent[] {
|
|
188
|
+
let events = [...this.eventBuffer];
|
|
189
|
+
if (filter?.kind) {
|
|
190
|
+
events = events.filter((e) => e.kind === filter.kind);
|
|
191
|
+
}
|
|
192
|
+
if (filter?.name) {
|
|
193
|
+
events = events.filter((e) => 'name' in e && e.name === filter.name);
|
|
194
|
+
}
|
|
195
|
+
if (filter?.since !== undefined) {
|
|
196
|
+
const since = filter.since;
|
|
197
|
+
events = events.filter(
|
|
198
|
+
(e) => 'timestamp' in e && typeof e.timestamp === 'number' && e.timestamp >= since
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (filter?.limit !== undefined) {
|
|
202
|
+
events = events.slice(-filter.limit);
|
|
203
|
+
}
|
|
204
|
+
return events;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getLastEvent(kind: string, name?: string): BrokerEvent | undefined {
|
|
208
|
+
for (let i = this.eventBuffer.length - 1; i >= 0; i -= 1) {
|
|
209
|
+
const event = this.eventBuffer[i];
|
|
210
|
+
if (event.kind === kind && (!name || ('name' in event && event.name === name))) {
|
|
211
|
+
return event;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|