agent-relay-server 0.4.13 → 0.4.14

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.
@@ -1,626 +0,0 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname, resolve } from "node:path";
3
- import { setTimeout as delay } from "node:timers/promises";
4
- import { CodexAppClient, type ClientEvent, type Thread, type ThreadStatus } from "./app-client";
5
- import { RelayClient, type RelayAgentStatus, type RelayMessage } from "./relay";
6
-
7
- interface Config {
8
- relayUrl: string;
9
- appServerUrl: string;
10
- cwd: string;
11
- rig: string;
12
- capabilities: string[];
13
- tags: string[];
14
- statePath: string;
15
- pollIntervalMs: number;
16
- heartbeatIntervalMs: number;
17
- coalesceWindowMs: number;
18
- reconnectInitialDelayMs: number;
19
- reconnectMaxDelayMs: number;
20
- threadMode: "auto" | "resume" | "start";
21
- threadId?: string;
22
- model?: string;
23
- approvalPolicy?: string;
24
- sandbox?: string;
25
- }
26
-
27
- interface RuntimeState {
28
- agentId: string;
29
- threadId: string;
30
- activeTurnId: string | null;
31
- threadStatus: ThreadStatus["type"];
32
- lastSeenMessageId: number;
33
- pendingMessageCount: number;
34
- appConnected: boolean;
35
- appServerUrl: string;
36
- relayUrl: string;
37
- cwd: string;
38
- updatedAt: string;
39
- }
40
-
41
- interface DeliveryBatch {
42
- messages: RelayMessage[];
43
- }
44
-
45
- class CodexLiveSidecar {
46
- private readonly relay: RelayClient;
47
- private readonly logPrefix = "[codex-live]";
48
- private app: CodexAppClient;
49
- private agentId = "";
50
- private threadId = "";
51
- private activeTurnId: string | null = null;
52
- private threadStatus: ThreadStatus["type"] = "notLoaded";
53
- private lastSeenMessageId = 0;
54
- private lastHeartbeatAt = 0;
55
- private stopping = false;
56
- private appConnected = false;
57
- private draining = false;
58
- private drainDueAt = 0;
59
- private reconnecting: Promise<void> | null = null;
60
- private readonly pendingMessages = new Map<number, RelayMessage>();
61
-
62
- constructor(private readonly config: Config) {
63
- this.relay = new RelayClient(config.relayUrl, (msg) => this.log(msg));
64
- this.app = this.createAppClient();
65
- }
66
-
67
- async run(): Promise<void> {
68
- mkdirSync(dirname(this.config.statePath), { recursive: true });
69
-
70
- process.on("SIGINT", () => void this.stop("SIGINT"));
71
- process.on("SIGTERM", () => void this.stop("SIGTERM"));
72
-
73
- await this.ensureAppReady();
74
-
75
- const registration = await this.relay.registerAgent({
76
- relayUrl: this.config.relayUrl,
77
- cwd: this.config.cwd,
78
- rig: this.config.rig,
79
- capabilities: this.config.capabilities,
80
- tags: this.config.tags,
81
- threadId: this.threadId,
82
- model: this.config.model,
83
- appServerUrl: this.config.appServerUrl,
84
- });
85
- this.agentId = registration.agentId;
86
-
87
- this.log(`registered agent ${this.agentId}`);
88
- this.log(`using thread ${this.threadId}`);
89
- this.writeState();
90
- await this.refreshRelayStatus();
91
-
92
- while (!this.stopping) {
93
- try {
94
- const now = Date.now();
95
- if (now - this.lastHeartbeatAt >= this.config.heartbeatIntervalMs) {
96
- await this.relay.heartbeat(this.agentId);
97
- this.lastHeartbeatAt = now;
98
- }
99
-
100
- const messages = await this.relay.pollMessages(this.agentId, this.lastSeenMessageId);
101
- if (messages.length > 0) {
102
- this.addPendingMessages(messages);
103
- this.writeState();
104
- }
105
-
106
- if (this.pendingMessages.size > 0 && now >= this.drainDueAt && !this.draining) {
107
- await this.drainPendingMessages();
108
- }
109
- } catch (error) {
110
- this.log(`loop error: ${describeError(error)}`);
111
- }
112
- await delay(this.config.pollIntervalMs);
113
- }
114
- }
115
-
116
- private createAppClient(): CodexAppClient {
117
- const app = new CodexAppClient(this.config.appServerUrl, (msg) => this.log(msg));
118
- app.onEvent((event) => this.handleAppEvent(event));
119
- app.onConnectionChange((connected) => {
120
- this.appConnected = connected;
121
- if (!connected) {
122
- this.activeTurnId = null;
123
- this.threadStatus = "notLoaded";
124
- if (!this.stopping) void this.ensureAppReady();
125
- }
126
- void this.refreshRelayStatus();
127
- this.writeState();
128
- });
129
- return app;
130
- }
131
-
132
- private async ensureAppReady(force = false): Promise<void> {
133
- if (this.stopping) return;
134
- if (this.appConnected && !force) return;
135
- if (this.reconnecting) return this.reconnecting;
136
-
137
- this.reconnecting = this.connectWithRetry(force).finally(() => {
138
- this.reconnecting = null;
139
- });
140
- return this.reconnecting;
141
- }
142
-
143
- private async connectWithRetry(force: boolean): Promise<void> {
144
- let waitMs = this.config.reconnectInitialDelayMs;
145
-
146
- while (!this.stopping) {
147
- try {
148
- if (force) {
149
- try {
150
- this.app.close();
151
- } catch {
152
- // Ignore close failures during forced reconnect.
153
- }
154
- this.appConnected = false;
155
- }
156
-
157
- if (!this.app.isConnected()) {
158
- this.app = this.createAppClient();
159
- await this.app.connect();
160
- await this.app.initialize();
161
- }
162
-
163
- const thread = this.threadId
164
- ? await this.resumeKnownThread(this.threadId)
165
- : await this.resolveThread();
166
- this.threadId = thread.id;
167
- this.syncThreadState(thread);
168
- if (this.agentId) {
169
- this.log(`app connection restored for thread ${thread.id}`);
170
- }
171
- return;
172
- } catch (error) {
173
- this.appConnected = false;
174
- this.log(`app connection failed: ${describeError(error)}; retrying in ${waitMs}ms`);
175
- await delay(waitMs);
176
- waitMs = Math.min(this.config.reconnectMaxDelayMs, waitMs * 2);
177
- }
178
- }
179
- }
180
-
181
- private async stop(signal: string): Promise<void> {
182
- if (this.stopping) return;
183
- this.stopping = true;
184
- this.log(`stopping on ${signal}`);
185
- if (this.agentId) {
186
- try {
187
- await this.relay.setStatus(this.agentId, "offline");
188
- } catch {
189
- // Best effort during shutdown.
190
- }
191
- }
192
- this.writeState();
193
- this.app.close();
194
- }
195
-
196
- private async resumeKnownThread(threadId: string): Promise<Thread> {
197
- try {
198
- const resumed = await this.app.threadResume({
199
- threadId,
200
- cwd: this.config.cwd,
201
- ...this.threadPermissions(),
202
- persistExtendedHistory: false,
203
- });
204
- return normalizeThread(resumed.thread);
205
- } catch (error) {
206
- this.log(`resume failed for thread ${threadId}: ${describeError(error)}; falling back to thread resolution`);
207
- this.threadId = "";
208
- return this.resolveThread();
209
- }
210
- }
211
-
212
- private async resolveThread(): Promise<Thread> {
213
- if (this.config.threadId && this.config.threadMode !== "start") {
214
- const resumed = await this.app.threadResume({
215
- threadId: this.config.threadId,
216
- cwd: this.config.cwd,
217
- ...this.threadPermissions(),
218
- persistExtendedHistory: false,
219
- });
220
- return normalizeThread(resumed.thread);
221
- }
222
-
223
- if (this.config.threadMode !== "start") {
224
- const loaded = await this.app.threadLoadedList(20);
225
- const loadedThreads: Thread[] = [];
226
- for (const loadedThreadId of loaded.data) {
227
- const thread = await this.readThreadWithFallback(loadedThreadId);
228
- if (thread.cwd === this.config.cwd) loadedThreads.push(thread);
229
- }
230
- const activeLoaded = loadedThreads
231
- .sort((a, b) => b.updatedAt - a.updatedAt)
232
- .find((thread) => thread.status.type === "active") ?? loadedThreads.sort((a, b) => b.updatedAt - a.updatedAt)[0];
233
- if (activeLoaded) return activeLoaded;
234
-
235
- const listed = await this.app.threadList({ cwd: this.config.cwd, limit: 10, archived: false });
236
- const latest = [...listed.data].sort((a, b) => b.updatedAt - a.updatedAt)[0];
237
- if (latest) {
238
- const resumed = await this.app.threadResume({
239
- threadId: latest.id,
240
- cwd: this.config.cwd,
241
- ...this.threadPermissions(),
242
- persistExtendedHistory: false,
243
- });
244
- return normalizeThread(resumed.thread);
245
- }
246
- }
247
-
248
- const started = await this.app.threadStart({
249
- cwd: this.config.cwd,
250
- ...this.threadPermissions(),
251
- ephemeral: false,
252
- sessionStartSource: "startup",
253
- model: this.config.model ?? null,
254
- });
255
- return normalizeThread(started.thread);
256
- }
257
-
258
- private threadPermissions(): Record<string, string> {
259
- const payload: Record<string, string> = {};
260
- if (this.config.approvalPolicy) payload.approvalPolicy = this.config.approvalPolicy;
261
- if (this.config.sandbox) payload.sandbox = this.config.sandbox;
262
- return payload;
263
- }
264
-
265
- private async readThreadWithFallback(threadId: string): Promise<Thread> {
266
- try {
267
- const read = await this.app.threadRead(threadId, true);
268
- return normalizeThread(read.thread);
269
- } catch (error) {
270
- if (!isThreadMaterializationError(error)) throw error;
271
- const read = await this.app.threadRead(threadId, false);
272
- return normalizeThread(read.thread);
273
- }
274
- }
275
-
276
- private syncThreadState(thread: Thread): void {
277
- this.threadStatus = thread.status.type;
278
- const turns = thread.turns ?? [];
279
- const activeTurn = [...turns].reverse().find((turn) => turn.status === "inProgress");
280
- this.activeTurnId = activeTurn?.id ?? null;
281
- void this.refreshRelayStatus();
282
- this.writeState();
283
- }
284
-
285
- private handleAppEvent(event: ClientEvent): void {
286
- if (event.type !== "notification") return;
287
- const { method, params } = event.message;
288
-
289
- if (method === "thread/status/changed") {
290
- if (params?.threadId === this.threadId && params.status && typeof params.status === "object" && "type" in params.status) {
291
- this.threadStatus = String(params.status.type) as ThreadStatus["type"];
292
- void this.refreshRelayStatus();
293
- this.writeState();
294
- }
295
- return;
296
- }
297
-
298
- if (method === "turn/started") {
299
- if (params?.threadId === this.threadId && params.turn && typeof params.turn === "object" && "id" in params.turn) {
300
- this.activeTurnId = String(params.turn.id);
301
- this.threadStatus = "active";
302
- void this.refreshRelayStatus();
303
- this.writeState();
304
- }
305
- return;
306
- }
307
-
308
- if (method === "turn/completed") {
309
- if (params?.threadId === this.threadId && params.turn && typeof params.turn === "object") {
310
- const completedId = "id" in params.turn ? String(params.turn.id) : null;
311
- if (completedId && this.activeTurnId === completedId) this.activeTurnId = null;
312
- this.threadStatus = "idle";
313
- void this.refreshRelayStatus();
314
- this.writeState();
315
- }
316
- }
317
- }
318
-
319
- private async refreshRelayStatus(): Promise<void> {
320
- if (!this.agentId) return;
321
-
322
- const status: RelayAgentStatus = !this.appConnected
323
- ? "online"
324
- : this.activeTurnId || this.threadStatus === "active"
325
- ? "busy"
326
- : "idle";
327
- const ready = this.appConnected && Boolean(this.threadId);
328
-
329
- try {
330
- await Promise.all([
331
- this.relay.setStatus(this.agentId, status),
332
- this.relay.setReady(this.agentId, ready),
333
- ]);
334
- } catch {
335
- // Best-effort status sync.
336
- }
337
- }
338
-
339
- private addPendingMessages(messages: RelayMessage[]): void {
340
- let added = false;
341
- for (const message of messages) {
342
- if (this.pendingMessages.has(message.id)) continue;
343
- this.pendingMessages.set(message.id, message);
344
- this.lastSeenMessageId = Math.max(this.lastSeenMessageId, message.id);
345
- added = true;
346
- }
347
-
348
- if (added && this.drainDueAt === 0) {
349
- this.drainDueAt = Date.now() + this.config.coalesceWindowMs;
350
- }
351
- }
352
-
353
- private async drainPendingMessages(): Promise<void> {
354
- if (this.draining || this.pendingMessages.size === 0) return;
355
-
356
- this.draining = true;
357
- const sorted = [...this.pendingMessages.values()].sort((a, b) => a.id - b.id);
358
- this.pendingMessages.clear();
359
- this.drainDueAt = 0;
360
-
361
- try {
362
- await this.ensureAppReady();
363
- const batches = createDeliveryBatches(sorted);
364
- for (let index = 0; index < batches.length; index += 1) {
365
- const batch = batches[index]!;
366
- try {
367
- await this.deliverBatch(batch);
368
- } catch (error) {
369
- this.requeueBatches(batches.slice(index));
370
- this.log(`delivery error: ${describeError(error)}`);
371
- await this.ensureAppReady(true);
372
- break;
373
- }
374
- }
375
- } finally {
376
- this.draining = false;
377
- this.writeState();
378
- }
379
- }
380
-
381
- private requeueBatches(batches: DeliveryBatch[]): void {
382
- for (const batch of batches) {
383
- for (const message of batch.messages) {
384
- this.pendingMessages.set(message.id, message);
385
- }
386
- }
387
- if (this.pendingMessages.size > 0) {
388
- this.drainDueAt = Date.now() + Math.min(this.config.coalesceWindowMs, 250);
389
- }
390
- }
391
-
392
- private async deliverBatch(batch: DeliveryBatch): Promise<void> {
393
- await this.ensureAppReady();
394
-
395
- for (const message of batch.messages) {
396
- if (!message.claimable) continue;
397
- const claimed = await this.relay.claimMessage(message.id, this.agentId);
398
- if (!claimed) {
399
- this.log(`skipping unclaimed message ${message.id}`);
400
- return;
401
- }
402
- }
403
-
404
- const delivery = this.pickDeliveryMode(batch.messages);
405
- const prompt = formatRelayPrompt(batch.messages);
406
- const ids = batch.messages.map((message) => message.id).join(", ");
407
- this.log(`delivering message ${ids} via ${delivery}`);
408
-
409
- if (delivery === "interrupt" && this.activeTurnId) {
410
- try {
411
- await this.app.turnInterrupt(this.threadId, this.activeTurnId);
412
- this.activeTurnId = null;
413
- this.threadStatus = "idle";
414
- await this.app.settle(200);
415
- } catch (error) {
416
- this.log(`interrupt failed for messages ${ids}: ${describeError(error)}`);
417
- }
418
- }
419
-
420
- if (delivery === "steer" && this.activeTurnId) {
421
- try {
422
- await this.app.turnSteer(this.threadId, this.activeTurnId, prompt);
423
- await this.markBatchRead(batch.messages);
424
- this.writeState();
425
- return;
426
- } catch (error) {
427
- this.log(`steer failed for messages ${ids}, falling back to start: ${describeError(error)}`);
428
- }
429
- }
430
-
431
- await this.app.turnStart(this.threadId, prompt);
432
- await this.markBatchRead(batch.messages);
433
- this.writeState();
434
- }
435
-
436
- private async markBatchRead(messages: RelayMessage[]): Promise<void> {
437
- for (const message of messages) {
438
- await this.relay.markRead(message.id, this.agentId);
439
- }
440
- }
441
-
442
- private pickDeliveryMode(messages: RelayMessage[]): "start" | "steer" | "interrupt" {
443
- const first = messages[0]!;
444
- const meta = first.meta ?? {};
445
- const requested = typeof meta.delivery === "string" ? meta.delivery : null;
446
- const priority = typeof meta.priority === "string" ? meta.priority : null;
447
-
448
- if (requested === "interrupt") return "interrupt";
449
- if (requested === "start") return "start";
450
- if (requested === "steer") return this.activeTurnId ? "steer" : "start";
451
- if (priority === "urgent") return this.activeTurnId ? "interrupt" : "start";
452
- return this.activeTurnId ? "steer" : "start";
453
- }
454
-
455
- private writeState(): void {
456
- if (!this.threadId) return;
457
- const state: RuntimeState = {
458
- agentId: this.agentId,
459
- threadId: this.threadId,
460
- activeTurnId: this.activeTurnId,
461
- threadStatus: this.threadStatus,
462
- lastSeenMessageId: this.lastSeenMessageId,
463
- pendingMessageCount: this.pendingMessages.size,
464
- appConnected: this.appConnected,
465
- appServerUrl: this.config.appServerUrl,
466
- relayUrl: this.config.relayUrl,
467
- cwd: this.config.cwd,
468
- updatedAt: new Date().toISOString(),
469
- };
470
- writeFileSync(this.config.statePath, JSON.stringify(state, null, 2));
471
- }
472
-
473
- private log(message: string): void {
474
- console.error(`${this.logPrefix} ${message}`);
475
- }
476
- }
477
-
478
- function createDeliveryBatches(messages: RelayMessage[]): DeliveryBatch[] {
479
- const batches: DeliveryBatch[] = [];
480
- let current: RelayMessage[] = [];
481
-
482
- for (const message of messages) {
483
- if (shouldIsolateMessage(message)) {
484
- if (current.length > 0) {
485
- batches.push({ messages: current });
486
- current = [];
487
- }
488
- batches.push({ messages: [message] });
489
- continue;
490
- }
491
-
492
- current.push(message);
493
- }
494
-
495
- if (current.length > 0) batches.push({ messages: current });
496
- return batches;
497
- }
498
-
499
- function shouldIsolateMessage(message: RelayMessage): boolean {
500
- if (message.claimable) return true;
501
- const meta = message.meta ?? {};
502
- const delivery = typeof meta.delivery === "string" ? meta.delivery : null;
503
- const priority = typeof meta.priority === "string" ? meta.priority : null;
504
- return delivery === "interrupt" || delivery === "start" || delivery === "steer" || priority === "urgent";
505
- }
506
-
507
- function formatRelayPrompt(messages: RelayMessage[]): string {
508
- if (messages.length === 1) {
509
- const message = messages[0]!;
510
- const lines = [
511
- "Agent Relay message received.",
512
- "",
513
- `Message ID: ${message.id}`,
514
- `From: ${message.from}`,
515
- `To: ${message.to}`,
516
- `Created: ${new Date(message.createdAt).toISOString()}`,
517
- ];
518
-
519
- if (message.subject) lines.push(`Subject: ${message.subject}`);
520
- if (message.replyTo) lines.push(`Reply To: ${message.replyTo}`);
521
- lines.push(
522
- "",
523
- "Body:",
524
- message.body,
525
- "",
526
- "Treat this as a live incoming message from another agent. Respond or act on it as appropriate.",
527
- "If AGENT_RELAY_TOKEN is set, include it as the X-Agent-Relay-Token header when calling the relay API.",
528
- `To reply, POST JSON to ${process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850"}/api/messages with from set to your Agent Relay ID, to set to ${JSON.stringify(message.from)}, and replyTo set to ${message.id}.`,
529
- );
530
- return lines.join("\n");
531
- }
532
-
533
- const lines = [
534
- "Agent Relay message batch received.",
535
- "",
536
- `Count: ${messages.length}`,
537
- "",
538
- ];
539
-
540
- for (const message of messages) {
541
- lines.push(
542
- `Message ID: ${message.id}`,
543
- `From: ${message.from}`,
544
- `To: ${message.to}`,
545
- `Created: ${new Date(message.createdAt).toISOString()}`,
546
- );
547
- if (message.subject) lines.push(`Subject: ${message.subject}`);
548
- if (message.replyTo) lines.push(`Reply To: ${message.replyTo}`);
549
- lines.push("Body:", message.body, "", "---", "");
550
- }
551
-
552
- lines.push(
553
- "Treat these as live incoming messages from other agents. Synthesize them into one coherent response or action.",
554
- "If AGENT_RELAY_TOKEN is set, include it as the X-Agent-Relay-Token header when calling the relay API.",
555
- `To reply, POST JSON to ${process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850"}/api/messages with from set to your Agent Relay ID and replyTo set to the message you are answering.`,
556
- );
557
- return lines.join("\n");
558
- }
559
-
560
- function normalizeThread(thread: Thread): Thread {
561
- return {
562
- ...thread,
563
- turns: thread.turns ?? [],
564
- };
565
- }
566
-
567
- function isThreadMaterializationError(error: unknown): boolean {
568
- return describeError(error).includes("not materialized yet");
569
- }
570
-
571
- function describeError(error: unknown): string {
572
- return error instanceof Error ? error.message : String(error);
573
- }
574
-
575
- function envNumber(env: NodeJS.ProcessEnv, name: string, fallback: number): number {
576
- const raw = env[name];
577
- if (!raw) return fallback;
578
- const parsed = Number(raw);
579
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
580
- }
581
-
582
- export function parseThreadMode(raw: string | undefined): Config["threadMode"] {
583
- if (raw === "auto" || raw === "resume" || raw === "start") return raw;
584
- return "start";
585
- }
586
-
587
- export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
588
- const cwd = env.CODEX_LIVE_CWD || process.cwd();
589
- const capabilities = (env.AGENT_RELAY_CAPS || "chat")
590
- .split(",")
591
- .map((value) => value.trim())
592
- .filter(Boolean);
593
-
594
- return {
595
- relayUrl: env.AGENT_RELAY_URL || "http://127.0.0.1:4850",
596
- appServerUrl: env.CODEX_APP_SERVER_URL || "ws://127.0.0.1:4501",
597
- cwd,
598
- rig: env.CODEX_LIVE_RIG || "codex-live",
599
- capabilities,
600
- tags: ["codex", env.CODEX_LIVE_RIG || "codex-live", cwd.split("/").filter(Boolean).at(-1) || "unknown"],
601
- statePath: env.CODEX_LIVE_STATE_PATH || resolve(cwd, "codex/runtime/live-state.json"),
602
- pollIntervalMs: envNumber(env, "CODEX_LIVE_POLL_INTERVAL_MS", 2000),
603
- heartbeatIntervalMs: envNumber(env, "CODEX_LIVE_HEARTBEAT_INTERVAL_MS", 30000),
604
- coalesceWindowMs: envNumber(env, "CODEX_LIVE_COALESCE_WINDOW_MS", 600),
605
- reconnectInitialDelayMs: envNumber(env, "CODEX_LIVE_RECONNECT_INITIAL_MS", 1000),
606
- reconnectMaxDelayMs: envNumber(env, "CODEX_LIVE_RECONNECT_MAX_MS", 10000),
607
- threadMode: parseThreadMode(env.CODEX_THREAD_MODE),
608
- threadId: env.CODEX_THREAD_ID || undefined,
609
- model: env.CODEX_MODEL || undefined,
610
- approvalPolicy: env.CODEX_LIVE_APPROVAL_POLICY || undefined,
611
- sandbox: env.CODEX_LIVE_SANDBOX || undefined,
612
- };
613
- }
614
-
615
- async function main(): Promise<void> {
616
- const config = loadConfig();
617
- const sidecar = new CodexLiveSidecar(config);
618
- await sidecar.run();
619
- }
620
-
621
- if (import.meta.main) {
622
- main().catch((error) => {
623
- console.error(error instanceof Error ? error.stack || error.message : String(error));
624
- process.exit(1);
625
- });
626
- }
@@ -1,40 +0,0 @@
1
- {
2
- "name": "agent-relay",
3
- "version": "0.4.11",
4
- "description": "Agent Relay integration for Codex sessions",
5
- "author": {
6
- "name": "Edin Mujkanovic"
7
- },
8
- "repository": "https://github.com/edimuj/agent-relay",
9
- "license": "AGPL-3.0-or-later",
10
- "keywords": [
11
- "agent-relay",
12
- "multi-agent",
13
- "codex",
14
- "codex-plugin",
15
- "ai-agents",
16
- "coding-agents",
17
- "inter-agent-communication",
18
- "agent-communication",
19
- "message-bus",
20
- "automation",
21
- "developer-tools"
22
- ],
23
- "skills": "./skills/",
24
- "interface": {
25
- "displayName": "Agent Relay",
26
- "shortDescription": "Connect Codex sessions to Agent Relay",
27
- "longDescription": "Adds Codex-facing instructions for Agent Relay and pairs with the agent-relay-codex launcher for live incoming messages.",
28
- "developerName": "Edin Mujkanovic",
29
- "category": "Productivity",
30
- "capabilities": [
31
- "Read",
32
- "Write"
33
- ],
34
- "defaultPrompt": [
35
- "Use Agent Relay to message another coding agent.",
36
- "Check how this Codex session is connected to Agent Relay."
37
- ],
38
- "brandColor": "#10A37F"
39
- }
40
- }
@@ -1,29 +0,0 @@
1
- ---
2
- name: agent-relay
3
- description: Use Agent Relay from Codex to understand live relay messages, reply to other agents, or send inter-agent messages through the local relay server.
4
- ---
5
-
6
- # Agent Relay
7
-
8
- Agent Relay is a local HTTP message bus for coding-agent sessions. When this Codex session is launched with `agent-relay-codex start`, a sidecar registers the session and injects incoming relay messages as live user turns.
9
-
10
- ## Incoming messages
11
-
12
- Treat prompts beginning with `Agent Relay message received` or `Agent Relay message batch received` as live messages from another agent. Act on the request if appropriate, and reply through the relay when a response is useful.
13
-
14
- ## Send a message
15
-
16
- Use the relay API at `${AGENT_RELAY_URL:-http://127.0.0.1:4850}`:
17
-
18
- ```bash
19
- curl -sS -X POST "${AGENT_RELAY_URL:-http://127.0.0.1:4850}/api/messages" \
20
- ${AGENT_RELAY_TOKEN:+-H "X-Agent-Relay-Token: ${AGENT_RELAY_TOKEN}"} \
21
- -H 'Content-Type: application/json' \
22
- -d '{"from":"<this-agent-id>","to":"<target>","subject":"<subject>","body":"<message>"}'
23
- ```
24
-
25
- Targets can be an agent id, `broadcast`, `cap:<capability>`, `tag:<tag>`, or `label:<label>`.
26
-
27
- ## Claimable tasks
28
-
29
- If an incoming relay message is described as claimable, only one matching agent should act on it. The Codex sidecar attempts to claim before delivery; if the claim fails, the message is skipped.