agent-anywhere-gateway 0.1.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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Agent Anywhere Gateway
2
+
3
+ Standalone gateway for Agent Anywhere controlled machines.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g agent-anywhere-gateway
9
+ ```
10
+
11
+ ## Run
12
+
13
+ ```bash
14
+ agent-anywhere-gateway \
15
+ --server https://agent-anywhere.example.com \
16
+ --id office-mac \
17
+ --token replace-me \
18
+ --allowed-roots /Users/me/gitrepo \
19
+ --provider codex
20
+ ```
21
+
22
+ One gateway can expose multiple local runtimes:
23
+
24
+ ```bash
25
+ agent-anywhere-gateway \
26
+ --server https://agent-anywhere.example.com \
27
+ --id office-mac \
28
+ --token replace-me \
29
+ --allowed-roots /Users/me/gitrepo \
30
+ --providers codex,claude-code
31
+ ```
32
+
33
+ `--server` accepts the Control Server or Worker URL and expands it to
34
+ `/api/gateways/{gateway_id}/ws`. Use `--server-ws-url` when you need to pass
35
+ the exact WebSocket URL.
36
+
37
+ The same settings can be provided with environment variables:
38
+
39
+ - `AGENT_ANYWHERE_SERVER_WS_URL`
40
+ - `AGENT_ANYWHERE_GATEWAY_ID`
41
+ - `AGENT_ANYWHERE_GATEWAY_TOKEN`
42
+ - `AGENT_ANYWHERE_ALLOWED_ROOTS`
43
+ - `AGENT_PROVIDER`
44
+ - `AGENT_PROVIDERS`
45
+
46
+ Version: 0.1.0
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { runCli } = require("../src/gateway/main");
4
+
5
+ try {
6
+ Promise.resolve(runCli()).catch((error) => {
7
+ console.error(error.stack || error.message);
8
+ process.exitCode = 1;
9
+ });
10
+ } catch (error) {
11
+ console.error(error.stack || error.message);
12
+ process.exitCode = 1;
13
+ }
@@ -0,0 +1,9 @@
1
+ # Copy this file to .cloudflare/gateway.env on each controlled machine.
2
+ # .cloudflare/ is ignored by git, so the gateway token stays local.
3
+
4
+ AGENT_ANYWHERE_GATEWAY_ID=helix-mac
5
+ AGENT_ANYWHERE_MACHINE_NAME=Helix Mac
6
+ AGENT_ANYWHERE_SERVER_WS_URL=wss://agent-anywhere.songofhawk.workers.dev/api/gateways/{gateway_id}/ws
7
+ AGENT_ANYWHERE_GATEWAY_TOKEN=replace-with-agent-anywhere-gateway-token
8
+ AGENT_ANYWHERE_ALLOWED_ROOTS=/Users/helix/gitrepo
9
+ AGENT_PROVIDERS=codex,claude-code
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "agent-anywhere-gateway",
3
+ "version": "0.1.0",
4
+ "description": "Standalone Agent Anywhere Gateway CLI for controlled machines.",
5
+ "main": "src/gateway/main.js",
6
+ "bin": {
7
+ "agent-anywhere-gateway": "bin/agent-anywhere-gateway.js"
8
+ },
9
+ "scripts": {
10
+ "start": "agent-anywhere-gateway",
11
+ "gateway": "node src/gateway.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "config",
19
+ "src",
20
+ "README.md",
21
+ "package.json"
22
+ ],
23
+ "dependencies": {
24
+ "@openai/codex-sdk": "^0.130.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }
@@ -0,0 +1,131 @@
1
+ const { buildCapabilities } = require("../shared/capabilities");
2
+ const { normalizeProviderName } = require("../shared/providers");
3
+ const { ClaudeCodeHeadlessRuntime } = require("../runtimes/claude-code-headless-runtime");
4
+ const { ClaudeCodeRuntime } = require("../runtimes/claude-code-runtime");
5
+ const { CodexAppServerRuntime } = require("../runtimes/codex-app-server-runtime");
6
+ const { CodexRuntime } = require("../runtimes/codex-runtime");
7
+ const { MockRuntime } = require("../runtimes/mock-runtime");
8
+
9
+ const LOCAL_ADAPTERS = {
10
+ codex: {
11
+ id: "codex",
12
+ kind: "local-process",
13
+ createRuntime: () => new CodexAppServerRuntime({ provider: "codex" })
14
+ },
15
+ "codex-sdk": {
16
+ id: "codex-sdk",
17
+ kind: "local-process",
18
+ createRuntime: () => new CodexRuntime({ provider: "codex-sdk" })
19
+ },
20
+ "codex-app-server": {
21
+ id: "codex-app-server",
22
+ kind: "local-process",
23
+ createRuntime: () => new CodexAppServerRuntime()
24
+ },
25
+ "claude-code": {
26
+ id: "claude-code",
27
+ kind: "local-process",
28
+ createRuntime: () => new ClaudeCodeRuntime()
29
+ },
30
+ "claude-code-headless": {
31
+ id: "claude-code-headless",
32
+ kind: "local-process",
33
+ createRuntime: () => new ClaudeCodeHeadlessRuntime()
34
+ },
35
+ mock: {
36
+ id: "mock",
37
+ kind: "local-process",
38
+ createRuntime: () => new MockRuntime()
39
+ }
40
+ };
41
+
42
+ class RuntimeAgentAdapter {
43
+ constructor({ id, kind, runtime }) {
44
+ this.id = id;
45
+ this.kind = kind;
46
+ this.runtime = runtime;
47
+ }
48
+
49
+ getCapabilities() {
50
+ return buildCapabilities(this.id);
51
+ }
52
+
53
+ async discoverCapabilities() {
54
+ if (typeof this.runtime.discoverCapabilities === "function") {
55
+ return this.runtime.discoverCapabilities();
56
+ }
57
+ return this.getCapabilities();
58
+ }
59
+
60
+ async *runTurn(context) {
61
+ yield* this.runtime.run({
62
+ session: context.session,
63
+ project: context.project,
64
+ message: context.message,
65
+ attachments: context.attachments,
66
+ settings: context.settings,
67
+ requestApproval: context.requestApproval
68
+ });
69
+ }
70
+
71
+ async cancelTurn(context) {
72
+ if (typeof this.runtime.cancelTurn !== "function") {
73
+ const error = new Error(`provider ${this.id} 不支持取消。`);
74
+ error.statusCode = 400;
75
+ throw error;
76
+ }
77
+ return this.runtime.cancelTurn(context);
78
+ }
79
+
80
+ async steerTurn(context) {
81
+ if (typeof this.runtime.steerTurn !== "function") {
82
+ const error = new Error(`provider ${this.id} 不支持运行中追加输入。`);
83
+ error.statusCode = 400;
84
+ throw error;
85
+ }
86
+ return this.runtime.steerTurn(context);
87
+ }
88
+
89
+ async listRuntimeSessions(context) {
90
+ if (typeof this.runtime.listRuntimeSessions !== "function") {
91
+ return [];
92
+ }
93
+ return this.runtime.listRuntimeSessions(context);
94
+ }
95
+
96
+ async readRuntimeSession(context) {
97
+ if (typeof this.runtime.readRuntimeSession !== "function") {
98
+ const error = new Error(`provider ${this.id} 不支持读取 runtime session。`);
99
+ error.statusCode = 400;
100
+ throw error;
101
+ }
102
+ return this.runtime.readRuntimeSession(context);
103
+ }
104
+ }
105
+
106
+ function createLocalAgentAdapter(provider) {
107
+ const normalizedProvider = normalizeProviderName(provider);
108
+ const definition = LOCAL_ADAPTERS[normalizedProvider];
109
+ if (!definition) {
110
+ const error = new Error(`不支持的 provider:${provider}`);
111
+ error.statusCode = 400;
112
+ throw error;
113
+ }
114
+
115
+ return new RuntimeAgentAdapter({
116
+ id: definition.id,
117
+ kind: definition.kind,
118
+ runtime: definition.createRuntime()
119
+ });
120
+ }
121
+
122
+ function listLocalAgentProviders() {
123
+ return Object.keys(LOCAL_ADAPTERS);
124
+ }
125
+
126
+ module.exports = {
127
+ LOCAL_ADAPTERS,
128
+ RuntimeAgentAdapter,
129
+ createLocalAgentAdapter,
130
+ listLocalAgentProviders
131
+ };
@@ -0,0 +1,422 @@
1
+ const os = require("node:os");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const { createLocalAgentAdapter } = require("../adapters/local-agent-adapter");
5
+ const { buildCapabilities, compactCapabilities } = require("../shared/capabilities");
6
+ const { GatewayMessageType } = require("../shared/gateway-protocol");
7
+ const { connectWebSocket } = require("../shared/websocket");
8
+ const { handleApprovalDecision, handleRequest } = require("./runner");
9
+ const { defaultProviderName, providerNames } = require("./providers");
10
+
11
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
12
+ const DEFAULT_RECONNECT_MIN_MS = 1_000;
13
+ const DEFAULT_RECONNECT_MAX_MS = 30_000;
14
+ const REPLAY_DIR_NAME = "gateway-replays";
15
+
16
+ function gatewayId() {
17
+ return process.env.AGENT_ANYWHERE_GATEWAY_ID || os.hostname() || "gateway";
18
+ }
19
+
20
+ function gatewayToken() {
21
+ return process.env.AGENT_ANYWHERE_GATEWAY_TOKEN || "please-change-me";
22
+ }
23
+
24
+ function providerName() {
25
+ return defaultProviderName();
26
+ }
27
+
28
+ function gatewayDataDir() {
29
+ return process.env.AGENT_ANYWHERE_DATA_DIR || path.join(process.cwd(), ".data");
30
+ }
31
+
32
+ function replayDir() {
33
+ return process.env.AGENT_ANYWHERE_GATEWAY_REPLAY_DIR || path.join(gatewayDataDir(), REPLAY_DIR_NAME);
34
+ }
35
+
36
+ function serverWsUrl() {
37
+ const id = gatewayId();
38
+ const raw = process.env.AGENT_ANYWHERE_SERVER_WS_URL || `ws://localhost:8787/api/gateways/${id}/ws`;
39
+ const url = new URL(raw.replace("{gateway_id}", encodeURIComponent(id)));
40
+ if (!url.searchParams.has("token")) {
41
+ url.searchParams.set("token", gatewayToken());
42
+ }
43
+ return url;
44
+ }
45
+
46
+ function registerUrl(wsUrl) {
47
+ const url = new URL(wsUrl);
48
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
49
+ url.pathname = "/api/machines/register";
50
+ url.search = "";
51
+ return url;
52
+ }
53
+
54
+ function machineRegistrationPayload(id, capabilities) {
55
+ return {
56
+ id,
57
+ name: process.env.AGENT_ANYWHERE_MACHINE_NAME || id,
58
+ host: "",
59
+ os: process.platform,
60
+ status: "online",
61
+ connection_mode: "gateway",
62
+ gateway_id: id,
63
+ capabilities
64
+ };
65
+ }
66
+
67
+ function mergeCapabilities(providerRows) {
68
+ const providers = [];
69
+ const models = [];
70
+ const inputModalities = [];
71
+ const reasoningEfforts = [];
72
+ const approvalPolicies = [];
73
+ const modes = [];
74
+ const providerCapabilities = {};
75
+ let defaultModel = null;
76
+ for (const row of providerRows) {
77
+ const capabilities = row.capabilities || buildCapabilities(row.provider);
78
+ providerCapabilities[row.provider] = capabilities;
79
+ for (const provider of capabilities.providers || [row.provider]) {
80
+ if (provider && !providers.includes(provider)) providers.push(provider);
81
+ }
82
+ for (const model of capabilities.models || []) {
83
+ if (model && !models.includes(model)) models.push(model);
84
+ }
85
+ for (const modality of capabilities.input_modalities || []) {
86
+ if (modality && !inputModalities.includes(modality)) inputModalities.push(modality);
87
+ }
88
+ for (const effort of capabilities.reasoning_efforts || []) {
89
+ if (effort && !reasoningEfforts.includes(effort)) reasoningEfforts.push(effort);
90
+ }
91
+ for (const policy of capabilities.approval_policies || []) {
92
+ if (policy && !approvalPolicies.includes(policy)) approvalPolicies.push(policy);
93
+ }
94
+ for (const mode of capabilities.modes || []) {
95
+ if (mode && !modes.includes(mode)) modes.push(mode);
96
+ }
97
+ if (!defaultModel && capabilities.default_model) {
98
+ defaultModel = capabilities.default_model;
99
+ }
100
+ }
101
+ const primaryProvider = providers[0] || providerName();
102
+ return compactCapabilities(primaryProvider, {
103
+ providers,
104
+ models,
105
+ default_model: defaultModel,
106
+ input_modalities: inputModalities,
107
+ reasoning_efforts: reasoningEfforts,
108
+ approval_policies: approvalPolicies,
109
+ modes,
110
+ provider_capabilities: providerCapabilities
111
+ });
112
+ }
113
+
114
+ async function discoverGatewayCapabilities(providers = providerNames()) {
115
+ const rows = [];
116
+ for (const provider of providers) {
117
+ const adapter = createLocalAgentAdapter(provider);
118
+ let capabilities;
119
+ try {
120
+ capabilities = await adapter.discoverCapabilities();
121
+ } catch {
122
+ capabilities = buildCapabilities(provider);
123
+ }
124
+ rows.push({ provider, capabilities });
125
+ }
126
+ return mergeCapabilities(rows);
127
+ }
128
+
129
+ async function registerMachine(wsUrl) {
130
+ const id = gatewayId();
131
+ const provider = providerName();
132
+ const capabilities = await discoverGatewayCapabilities();
133
+ const url = registerUrl(wsUrl);
134
+ const headers = {
135
+ "Content-Type": "application/json",
136
+ "x-agent-anywhere-gateway-token": gatewayToken()
137
+ };
138
+ const post = (payload) => fetch(url, {
139
+ method: "POST",
140
+ headers,
141
+ body: JSON.stringify(payload)
142
+ });
143
+
144
+ let response = await post(machineRegistrationPayload(id, capabilities));
145
+
146
+ if (!response.ok) {
147
+ const body = await response.text();
148
+ if (/SQLITE_TOOBIG|string or blob too big/i.test(body)) {
149
+ const fallback = compactCapabilities(provider, buildCapabilities(provider));
150
+ response = await post(machineRegistrationPayload(id, fallback));
151
+ if (response.ok) {
152
+ return;
153
+ }
154
+ const retryBody = await response.text();
155
+ throw new Error(`gateway 注册失败:${response.status} ${retryBody}`);
156
+ }
157
+ throw new Error(`gateway 注册失败:${response.status} ${body}`);
158
+ }
159
+ }
160
+
161
+ function replayFileName({ gateway_id, session_id, turn_id }) {
162
+ return [
163
+ gateway_id || gatewayId(),
164
+ session_id || "session",
165
+ turn_id || "turn",
166
+ Date.now(),
167
+ Math.random().toString(36).slice(2)
168
+ ].map((part) => String(part).replace(/[^A-Za-z0-9_.-]/g, "_")).join("__") + ".json";
169
+ }
170
+
171
+ function saveRunReplay(replay, { dir = replayDir() } = {}) {
172
+ if (!replay?.events?.length && !replay?.terminal_status) {
173
+ return null;
174
+ }
175
+ fs.mkdirSync(dir, { recursive: true });
176
+ const filePath = path.join(dir, replayFileName(replay));
177
+ fs.writeFileSync(filePath, JSON.stringify(replay, null, 2));
178
+ return filePath;
179
+ }
180
+
181
+ function listRunReplayFiles({ dir = replayDir() } = {}) {
182
+ if (!fs.existsSync(dir)) {
183
+ return [];
184
+ }
185
+ return fs.readdirSync(dir)
186
+ .filter((name) => name.endsWith(".json"))
187
+ .map((name) => path.join(dir, name))
188
+ .sort();
189
+ }
190
+
191
+ function flushRunReplays(socket, { dir = replayDir() } = {}) {
192
+ if (socket.closed || socket.socket?.destroyed) {
193
+ return 0;
194
+ }
195
+ let sent = 0;
196
+ for (const filePath of listRunReplayFiles({ dir })) {
197
+ const replay = JSON.parse(fs.readFileSync(filePath, "utf8"));
198
+ socket.sendJson({
199
+ type: GatewayMessageType.RUN_REPLAY,
200
+ payload: replay
201
+ });
202
+ fs.unlinkSync(filePath);
203
+ sent += 1;
204
+ }
205
+ return sent;
206
+ }
207
+
208
+ async function handleMessage(raw, socket, { isConnected = () => true, saveReplay = saveRunReplay } = {}) {
209
+ const message = JSON.parse(raw);
210
+ if (message.type === GatewayMessageType.APPROVAL_DECISION) {
211
+ handleApprovalDecision(message);
212
+ return;
213
+ }
214
+ if (message.type !== GatewayMessageType.REQUEST) {
215
+ return;
216
+ }
217
+
218
+ const replay = {
219
+ gateway_id: gatewayId(),
220
+ session_id: message.payload?.session?.id || null,
221
+ turn_id: message.payload?.turn?.id || null,
222
+ events: [],
223
+ terminal_status: null,
224
+ error: null
225
+ };
226
+ let shouldReplay = false;
227
+ const rememberForReplay = (payload) => {
228
+ if (payload.type === GatewayMessageType.STREAM_EVENT) {
229
+ replay.events.push(payload.payload);
230
+ if (payload.payload?.type === "cancelled") replay.terminal_status = "cancelled";
231
+ if (payload.payload?.type === "error") replay.terminal_status = "failed";
232
+ if (payload.payload?.type === "complete" && !replay.terminal_status) replay.terminal_status = "completed";
233
+ } else if (payload.type === GatewayMessageType.STREAM_COMPLETE) {
234
+ replay.terminal_status = replay.terminal_status || "completed";
235
+ } else if (payload.type === GatewayMessageType.STREAM_ERROR) {
236
+ replay.terminal_status = "failed";
237
+ replay.error = payload.error || null;
238
+ }
239
+ };
240
+
241
+ const send = (payload) => {
242
+ const connected = isConnected() && !socket.closed && !socket.socket?.destroyed;
243
+ if (!connected) {
244
+ shouldReplay = true;
245
+ rememberForReplay(payload);
246
+ return false;
247
+ }
248
+ try {
249
+ socket.sendJson({
250
+ request_id: message.request_id,
251
+ ...payload
252
+ });
253
+ return true;
254
+ } catch {
255
+ shouldReplay = true;
256
+ rememberForReplay(payload);
257
+ return false;
258
+ }
259
+ };
260
+
261
+ await handleRequest(message, send, { gatewayId: gatewayId() });
262
+ if (shouldReplay && (replay.events.length || replay.terminal_status)) {
263
+ saveReplay(replay);
264
+ }
265
+ }
266
+
267
+ function positiveInteger(value, fallback) {
268
+ const parsed = Number.parseInt(value, 10);
269
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
270
+ }
271
+
272
+ function reconnectDelayMs(failureCount, { minMs = DEFAULT_RECONNECT_MIN_MS, maxMs = DEFAULT_RECONNECT_MAX_MS } = {}) {
273
+ const min = positiveInteger(minMs, DEFAULT_RECONNECT_MIN_MS);
274
+ const max = Math.max(min, positiveInteger(maxMs, DEFAULT_RECONNECT_MAX_MS));
275
+ return Math.min(max, min * (2 ** Math.max(0, failureCount - 1)));
276
+ }
277
+
278
+ function wait(ms, signal) {
279
+ if (signal?.aborted || ms <= 0) {
280
+ return Promise.resolve();
281
+ }
282
+ return new Promise((resolve) => {
283
+ const timer = setTimeout(done, ms);
284
+ function done() {
285
+ clearTimeout(timer);
286
+ signal?.removeEventListener?.("abort", done);
287
+ resolve();
288
+ }
289
+ signal?.addEventListener?.("abort", done, { once: true });
290
+ });
291
+ }
292
+
293
+ function logError(log, message) {
294
+ if (typeof log?.error === "function") {
295
+ log.error(message);
296
+ }
297
+ }
298
+
299
+ function logInfo(log, message) {
300
+ if (typeof log?.log === "function") {
301
+ log.log(message);
302
+ }
303
+ }
304
+
305
+ async function runGatewayConnection({
306
+ connect = connectWebSocket,
307
+ heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
308
+ log = console,
309
+ register = registerMachine,
310
+ wsUrl = serverWsUrl()
311
+ } = {}) {
312
+ await register(wsUrl);
313
+ const socket = await connect(wsUrl);
314
+ const connectedAt = Date.now();
315
+ let heartbeatTimer = null;
316
+ let settled = false;
317
+ let connected = true;
318
+ let closeConnection = () => {};
319
+
320
+ const finish = (resolve, reason) => {
321
+ if (settled) {
322
+ return;
323
+ }
324
+ settled = true;
325
+ connected = false;
326
+ if (heartbeatTimer) {
327
+ clearInterval(heartbeatTimer);
328
+ heartbeatTimer = null;
329
+ }
330
+ resolve({ reason, connectedForMs: Date.now() - connectedAt });
331
+ };
332
+
333
+ const closed = new Promise((resolve) => {
334
+ closeConnection = (reason) => finish(resolve, reason);
335
+ socket.on("message", (raw) => {
336
+ handleMessage(raw, socket, { isConnected: () => connected }).catch((error) => {
337
+ logError(log, error.stack || error.message);
338
+ });
339
+ });
340
+ socket.on("close", () => {
341
+ logError(log, "Agent Gateway disconnected.");
342
+ closeConnection("close");
343
+ });
344
+ socket.on("error", (error) => {
345
+ logError(log, error?.stack || "Agent Gateway connection error.");
346
+ closeConnection("error");
347
+ });
348
+ });
349
+
350
+ const heartbeat = () => {
351
+ try {
352
+ flushRunReplays(socket);
353
+ socket.sendJson({
354
+ type: GatewayMessageType.HEARTBEAT,
355
+ machine_id: gatewayId()
356
+ });
357
+ } catch (error) {
358
+ logError(log, error?.stack || error?.message || "Agent Gateway heartbeat failed.");
359
+ closeConnection("heartbeat_error");
360
+ if (typeof socket.close === "function") {
361
+ socket.close();
362
+ }
363
+ }
364
+ };
365
+
366
+ heartbeat();
367
+ heartbeatTimer = setInterval(heartbeat, heartbeatIntervalMs);
368
+ logInfo(log, `Agent Gateway connected: ${wsUrl.origin}`);
369
+
370
+ return closed;
371
+ }
372
+
373
+ async function startGateway(options = {}) {
374
+ const {
375
+ heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
376
+ log = console,
377
+ reconnectMaxMs = process.env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MAX_MS || DEFAULT_RECONNECT_MAX_MS,
378
+ reconnectMinMs = process.env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MIN_MS || DEFAULT_RECONNECT_MIN_MS,
379
+ signal
380
+ } = options;
381
+ const wsUrl = options.wsUrl || serverWsUrl();
382
+ let failureCount = 0;
383
+
384
+ while (!signal?.aborted) {
385
+ try {
386
+ const result = await runGatewayConnection({
387
+ ...options,
388
+ heartbeatIntervalMs,
389
+ log,
390
+ wsUrl
391
+ });
392
+ failureCount = result.connectedForMs >= positiveInteger(reconnectMinMs, DEFAULT_RECONNECT_MIN_MS)
393
+ ? 1
394
+ : failureCount + 1;
395
+ } catch (error) {
396
+ failureCount += 1;
397
+ logError(log, error.stack || error.message);
398
+ }
399
+
400
+ if (signal?.aborted) {
401
+ break;
402
+ }
403
+ const delay = reconnectDelayMs(failureCount, { minMs: reconnectMinMs, maxMs: reconnectMaxMs });
404
+ logError(log, `Agent Gateway reconnecting in ${delay}ms.`);
405
+ await wait(delay, signal);
406
+ }
407
+ }
408
+
409
+ module.exports = {
410
+ discoverGatewayCapabilities,
411
+ gatewayId,
412
+ handleMessage,
413
+ mergeCapabilities,
414
+ reconnectDelayMs,
415
+ registerMachine,
416
+ registerUrl,
417
+ flushRunReplays,
418
+ runGatewayConnection,
419
+ saveRunReplay,
420
+ serverWsUrl,
421
+ startGateway
422
+ };