agent-relay-codex 0.6.0 → 0.10.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.
@@ -1,63 +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
- ## Pair with another session
28
-
29
- Use pair sessions for focused, two-agent live collaboration. Pairing is exclusive: one agent can have only one pending or active pair.
30
-
31
- When the user types a slash-style relay instruction such as `/pair codex`,
32
- `/message codex ...`, `/send-claimable tag:backend ...`, `/pair status`,
33
- `/pair send <pair-id> ...`, `/disconnect`, `/status`, `/label ...`, or
34
- `/tags ...`, run the matching `agent-relay` CLI command instead of explaining
35
- the API. The CLI auto-detects the current agent id from provider state in normal
36
- plugin installs.
37
-
38
- ```bash
39
- agent-relay /pair codex "Debug flaky tests"
40
- agent-relay /message codex "Can you look at that failing action?"
41
- agent-relay /send-claimable tag:backend "Please claim and fix the failing API test"
42
- agent-relay /pair status
43
- agent-relay /pair send PAIR_ID "What do you see?"
44
- agent-relay /disconnect
45
- agent-relay /status
46
- agent-relay /label backend-fixer
47
- agent-relay /tags backend tests urgent
48
- ```
49
-
50
- Create an invite:
51
-
52
- ```bash
53
- curl -sS -X POST "${AGENT_RELAY_URL:-http://127.0.0.1:4850}/api/pairs" \
54
- ${AGENT_RELAY_TOKEN:+-H "X-Agent-Relay-Token: ${AGENT_RELAY_TOKEN}"} \
55
- -H 'Content-Type: application/json' \
56
- -d '{"from":"<this-agent-id>","target":"codex","objective":"<what to solve together>"}'
57
- ```
58
-
59
- Accept/reject incoming pair invites with `/api/pairs/<pair-id>/accept` or `/api/pairs/<pair-id>/reject` and `{"agentId":"<this-agent-id>"}`. Send pair chat through `/api/pairs/<pair-id>/messages` with `{"from":"<this-agent-id>","body":"..."}`. Hang up with `/api/pairs/<pair-id>/hangup`.
60
-
61
- ## Claimable tasks
62
-
63
- 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.
@@ -1,16 +0,0 @@
1
- ---
2
- name: disconnect
3
- description: End the current Agent Relay pair session. Use when the user invokes /disconnect or asks to hang up, unpair, or disconnect from a paired agent.
4
- argument-hint: "[PAIR_ID]"
5
- allowed-tools: [Bash]
6
- ---
7
-
8
- # Agent Relay Disconnect
9
-
10
- Run:
11
-
12
- ```bash
13
- agent-relay /disconnect $ARGUMENTS
14
- ```
15
-
16
- If no pair id is supplied, the CLI ends the active pair for this session. Report the result briefly.
@@ -1,23 +0,0 @@
1
- ---
2
- name: label
3
- description: Read, set, or clear the current Agent Relay agent label. Use when the user invokes /label or asks to rename this relay agent.
4
- argument-hint: "[LABEL|--clear]"
5
- allowed-tools: [Bash]
6
- ---
7
-
8
- # Agent Relay Label
9
-
10
- Run:
11
-
12
- ```bash
13
- agent-relay /label $ARGUMENTS
14
- ```
15
-
16
- Examples:
17
-
18
- ```bash
19
- agent-relay /label backend-fixer
20
- agent-relay /label --clear
21
- ```
22
-
23
- Report the new label or current label briefly.
@@ -1,24 +0,0 @@
1
- ---
2
- name: message
3
- description: Send a normal Agent Relay message to another agent, label, tag, capability, or broadcast target. Use when the user invokes /message or asks to send a one-off relay message.
4
- argument-hint: "<target> <message> [--subject TEXT] [--channel NAME]"
5
- allowed-tools: [Bash]
6
- ---
7
-
8
- # Agent Relay Message
9
-
10
- Run:
11
-
12
- ```bash
13
- agent-relay /message $ARGUMENTS
14
- ```
15
-
16
- Examples:
17
-
18
- ```bash
19
- agent-relay /message codex "Can you look at that failing action?"
20
- agent-relay /message tag:backend "Does anyone see the regression?"
21
- agent-relay /message broadcast "Standup in five minutes"
22
- ```
23
-
24
- Report the sent message id briefly.
@@ -1,26 +0,0 @@
1
- ---
2
- name: pair
3
- description: Start, inspect, accept, reject, or send messages in an Agent Relay two-agent pair session. Use when the user invokes /pair or asks to pair this agent with Codex, Claude, or another relay agent.
4
- argument-hint: "<target|status|accept|reject|send> [args]"
5
- allowed-tools: [Bash]
6
- ---
7
-
8
- # Agent Relay Pair
9
-
10
- Run the Agent Relay CLI with the user's arguments:
11
-
12
- ```bash
13
- agent-relay /pair $ARGUMENTS
14
- ```
15
-
16
- Use this for pair-session commands such as:
17
-
18
- ```bash
19
- agent-relay /pair codex "Debug flaky tests"
20
- agent-relay /pair status
21
- agent-relay /pair accept PAIR_ID
22
- agent-relay /pair reject PAIR_ID
23
- agent-relay /pair send PAIR_ID "What do you see?"
24
- ```
25
-
26
- Report the command output briefly. If the CLI cannot detect this session's agent id, rerun with `--agent AGENT_ID` or `--from AGENT_ID` using the Agent Relay ID shown in session context.
@@ -1,24 +0,0 @@
1
- ---
2
- name: send-claimable
3
- description: Send a claimable Agent Relay work item so one matching agent can claim and handle it. Use when the user invokes /send-claimable or wants to enqueue work for another agent.
4
- argument-hint: "<target> <message> [--subject TEXT] [--channel NAME]"
5
- allowed-tools: [Bash]
6
- ---
7
-
8
- # Agent Relay Send Claimable
9
-
10
- Run:
11
-
12
- ```bash
13
- agent-relay /send-claimable $ARGUMENTS
14
- ```
15
-
16
- Examples:
17
-
18
- ```bash
19
- agent-relay /send-claimable codex "Claim this and inspect the failing action"
20
- agent-relay /send-claimable tag:backend "Fix the failing API test"
21
- agent-relay /send-claimable cap:review "Review the migration patch"
22
- ```
23
-
24
- Report the sent claimable message id briefly.
@@ -1,16 +0,0 @@
1
- ---
2
- name: status
3
- description: Show Agent Relay status for this session, including relay health, current agent id, label, tags, readiness, and active pair state. Use when the user invokes /status or asks for relay connection status.
4
- argument-hint: "[--json]"
5
- allowed-tools: [Bash]
6
- ---
7
-
8
- # Agent Relay Status
9
-
10
- Run:
11
-
12
- ```bash
13
- agent-relay /status $ARGUMENTS
14
- ```
15
-
16
- Summarize the current relay connection, agent identity, label, tags, and active pair state.
@@ -1,25 +0,0 @@
1
- ---
2
- name: tags
3
- description: List or update Agent Relay tags for the current session. Use when the user invokes /tags or asks to set, add, remove, or inspect relay tags.
4
- argument-hint: "[TAG ...|--list|--add TAGS|--remove TAGS]"
5
- allowed-tools: [Bash]
6
- ---
7
-
8
- # Agent Relay Tags
9
-
10
- Run:
11
-
12
- ```bash
13
- agent-relay /tags $ARGUMENTS
14
- ```
15
-
16
- Examples:
17
-
18
- ```bash
19
- agent-relay /tags
20
- agent-relay /tags backend tests urgent
21
- agent-relay /tags --add backend,tests
22
- agent-relay /tags --remove urgent
23
- ```
24
-
25
- Report the resulting tags briefly.
package/profile.ts DELETED
@@ -1,96 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { parseApprovalMode, type ApprovalMode } from "./approval";
4
-
5
- interface AgentRelayProfile {
6
- label?: string;
7
- tags?: string[];
8
- capabilities?: string[];
9
- channels?: string[];
10
- approval?: ApprovalMode;
11
- meta?: Record<string, unknown>;
12
- }
13
-
14
- interface ResolvedAgentProfile {
15
- profileName?: string;
16
- label?: string;
17
- tags: string[];
18
- capabilities: string[];
19
- channels: string[];
20
- approval?: ApprovalMode;
21
- meta: Record<string, unknown>;
22
- }
23
-
24
- function splitCsv(raw: string | undefined): string[] {
25
- if (!raw) return [];
26
- return [...new Set(raw.split(",").map((value) => value.trim()).filter(Boolean))];
27
- }
28
-
29
- function stringArray(value: unknown): string[] {
30
- if (!Array.isArray(value)) return [];
31
- return [...new Set(value.filter((item): item is string => typeof item === "string" && item.trim().length > 0).map((item) => item.trim()))];
32
- }
33
-
34
- function cleanLabel(value: unknown): string | undefined {
35
- if (typeof value !== "string") return undefined;
36
- const trimmed = value.trim();
37
- return trimmed ? trimmed : undefined;
38
- }
39
-
40
- function cleanMeta(value: unknown): Record<string, unknown> {
41
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
42
- }
43
-
44
- function loadProfileFile(env: NodeJS.ProcessEnv): Record<string, unknown> {
45
- const file = env.AGENT_RELAY_PROFILES_FILE || join(env.HOME || "", ".config", "agent-relay", "profiles.json");
46
- if (!file || !existsSync(file)) return {};
47
- try {
48
- const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
49
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
50
- } catch {
51
- return {};
52
- }
53
- }
54
-
55
- export function loadAgentRelayProfile(
56
- env: NodeJS.ProcessEnv,
57
- defaults: { provider: string; rig: string; project: string },
58
- ): ResolvedAgentProfile {
59
- const profileName = cleanLabel(env.AGENT_RELAY_PROFILE);
60
- const rawProfile = profileName ? loadProfileFile(env)[profileName] : undefined;
61
- const profile = cleanMeta(rawProfile);
62
- const defaultTags = [defaults.provider, defaults.rig, defaults.project].filter(Boolean);
63
- const profileTags = stringArray(profile.tags);
64
- const envTags = splitCsv(env.AGENT_RELAY_TAGS);
65
- const profileCapabilities = stringArray(profile.capabilities);
66
- const profileChannels = stringArray(profile.channels);
67
- const envChannels = splitCsv(env.AGENT_RELAY_CHANNELS);
68
- const profileApproval = cleanLabel(profile.approval);
69
-
70
- return {
71
- profileName,
72
- label: cleanLabel(env.AGENT_RELAY_LABEL) ?? cleanLabel(profile.label),
73
- tags: [...new Set([...defaultTags, ...(envTags.length ? envTags : profileTags)])],
74
- capabilities: splitCsv(env.AGENT_RELAY_CAPS).length
75
- ? splitCsv(env.AGENT_RELAY_CAPS)
76
- : profileCapabilities.length
77
- ? profileCapabilities
78
- : ["chat"],
79
- channels: envChannels.length ? envChannels : profileChannels,
80
- approval: env.AGENT_RELAY_APPROVAL
81
- ? parseApprovalMode(env.AGENT_RELAY_APPROVAL)
82
- : profileApproval
83
- ? parseApprovalMode(profileApproval)
84
- : undefined,
85
- meta: {
86
- ...cleanMeta(profile.meta),
87
- ...(profileName ? { profile: profileName } : {}),
88
- },
89
- };
90
- }
91
-
92
- export function messageMatchesProfileChannels(message: { channel?: string }, channels: string[]): boolean {
93
- if (channels.length === 0) return true;
94
- if (!message.channel) return true;
95
- return channels.includes(message.channel);
96
- }
package/relay.ts DELETED
@@ -1,188 +0,0 @@
1
- import { hostname } from "node:os";
2
-
3
- export type RelayAgentStatus = "online" | "idle" | "busy" | "offline";
4
-
5
- export interface RelayMessage {
6
- id: number;
7
- from: string;
8
- to: string;
9
- kind: string;
10
- channel?: string;
11
- subject?: string;
12
- body: string;
13
- replyTo?: number;
14
- claimable?: boolean;
15
- claimedBy?: string;
16
- claimedAt?: number;
17
- claimExpiresAt?: number;
18
- payload?: Record<string, unknown>;
19
- meta?: Record<string, unknown>;
20
- createdAt: number;
21
- }
22
-
23
- interface RelayConfig {
24
- relayUrl: string;
25
- cwd: string;
26
- rig: string;
27
- label?: string;
28
- capabilities: string[];
29
- tags: string[];
30
- channels?: string[];
31
- profileName?: string;
32
- profileMeta?: Record<string, unknown>;
33
- threadId: string;
34
- model?: string;
35
- appServerUrl: string;
36
- approvalMode?: string;
37
- instanceId?: string;
38
- }
39
-
40
- export interface RelayAgentSession {
41
- instanceId: string;
42
- epoch: number;
43
- }
44
-
45
- export function buildAgentIdentity(config: RelayConfig): { id: string; name: string; machine: string; project: string } {
46
- const machine = hostname();
47
- const project = config.cwd.split("/").filter(Boolean).at(-1) || "unknown";
48
- const shortThread = config.threadId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "thread";
49
- const id = `${machine}-${config.rig}-${project}-${shortThread}`;
50
- const name = `${project} (${config.rig} @ ${machine})`;
51
- return { id, name, machine, project };
52
- }
53
-
54
- export class RelayClient {
55
- constructor(private readonly baseUrl: string, private readonly log: (msg: string) => void = () => {}) {}
56
-
57
- async registerAgent(config: RelayConfig): Promise<{ agentId: string; project: string; session?: RelayAgentSession }> {
58
- const identity = buildAgentIdentity(config);
59
- const payload = {
60
- id: identity.id,
61
- name: identity.name,
62
- label: config.label,
63
- machine: identity.machine,
64
- rig: config.rig,
65
- tags: config.tags,
66
- capabilities: config.capabilities,
67
- status: "online",
68
- instanceId: config.instanceId,
69
- meta: {
70
- provider: "codex",
71
- client: "codex-live",
72
- threadId: config.threadId,
73
- appServerUrl: config.appServerUrl,
74
- cwd: config.cwd,
75
- model: config.model || null,
76
- approvalMode: config.approvalMode || "open",
77
- channels: config.channels ?? [],
78
- profile: config.profileName || null,
79
- ...(config.profileMeta ?? {}),
80
- },
81
- };
82
-
83
- const agent = await this.json("POST", "/api/agents", payload) as { epoch?: unknown } | null;
84
- const epoch = typeof agent?.epoch === "number" && Number.isSafeInteger(agent.epoch) ? agent.epoch : undefined;
85
- return {
86
- agentId: identity.id,
87
- project: identity.project,
88
- session: config.instanceId && epoch !== undefined ? { instanceId: config.instanceId, epoch } : undefined,
89
- };
90
- }
91
-
92
- async heartbeat(agentId: string, session?: RelayAgentSession): Promise<void> {
93
- await this.json("POST", `/api/agents/${encodeURIComponent(agentId)}/heartbeat`, session);
94
- }
95
-
96
- async getCursor(): Promise<number> {
97
- const result = await this.json("GET", "/api/messages/cursor") as { latestId?: unknown } | null;
98
- return typeof result?.latestId === "number" && Number.isSafeInteger(result.latestId) ? result.latestId : 0;
99
- }
100
-
101
- async setStatus(agentId: string, status: RelayAgentStatus, session?: RelayAgentSession): Promise<void> {
102
- await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/status`, { status, ...session });
103
- }
104
-
105
- async setReady(agentId: string, ready: boolean, session?: RelayAgentSession): Promise<void> {
106
- await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/ready`, { ready, ...session });
107
- }
108
-
109
- async unregisterAgent(agentId: string): Promise<void> {
110
- await this.json("DELETE", `/api/agents/${encodeURIComponent(agentId)}`);
111
- }
112
-
113
- async pollMessages(agentId: string, sinceId: number): Promise<RelayMessage[]> {
114
- const url = new URL(`/api/messages`, this.baseUrl);
115
- url.searchParams.set("for", agentId);
116
- url.searchParams.set("unread", "true");
117
- if (sinceId > 0) url.searchParams.set("sinceId", String(sinceId));
118
- const response = await fetch(url, { headers: this.headers() });
119
- if (!response.ok) {
120
- throw new RelayHttpError("GET", "/api/messages", response.status, response.statusText, await response.text());
121
- }
122
- return (await response.json()) as RelayMessage[];
123
- }
124
-
125
- async claimMessage(messageId: number, agentId: string, session?: RelayAgentSession): Promise<boolean> {
126
- const response = await fetch(new URL(`/api/messages/${messageId}/claim`, this.baseUrl), {
127
- method: "POST",
128
- headers: this.headers({ "Content-Type": "application/json" }),
129
- body: JSON.stringify({ agentId, ...session }),
130
- });
131
-
132
- if (response.ok) return true;
133
- if (response.status === 409) return false;
134
- if (response.status === 400 || response.status === 404) return false;
135
- throw new RelayHttpError("POST", `/api/messages/${messageId}/claim`, response.status, response.statusText, await response.text());
136
- }
137
-
138
- async renewMessageClaim(messageId: number, agentId: string, session?: RelayAgentSession): Promise<boolean> {
139
- const response = await fetch(new URL(`/api/messages/${messageId}/claim/renew`, this.baseUrl), {
140
- method: "POST",
141
- headers: this.headers({ "Content-Type": "application/json" }),
142
- body: JSON.stringify({ agentId, ...session }),
143
- });
144
-
145
- if (response.ok) return true;
146
- if (response.status === 409) return false;
147
- if (response.status === 400 || response.status === 404) return false;
148
- throw new RelayHttpError("POST", `/api/messages/${messageId}/claim/renew`, response.status, response.statusText, await response.text());
149
- }
150
-
151
- async markRead(messageId: number, agentId: string): Promise<void> {
152
- await this.json("PATCH", `/api/messages/${messageId}`, { readBy: agentId });
153
- }
154
-
155
- private async json(method: string, path: string, body?: unknown): Promise<unknown> {
156
- const response = await fetch(new URL(path, this.baseUrl), {
157
- method,
158
- headers: this.headers({ "Content-Type": "application/json" }),
159
- body: body === undefined ? undefined : JSON.stringify(body),
160
- });
161
-
162
- if (!response.ok) {
163
- const text = await response.text();
164
- this.log(`relay error ${method} ${path}: ${response.status} ${text}`);
165
- throw new RelayHttpError(method, path, response.status, response.statusText, text);
166
- }
167
-
168
- if (response.status === 204) return null;
169
- return response.json().catch(() => null);
170
- }
171
-
172
- private headers(base: Record<string, string> = {}): Record<string, string> {
173
- const token = process.env.AGENT_RELAY_TOKEN;
174
- return token ? { ...base, "X-Agent-Relay-Token": token } : base;
175
- }
176
- }
177
-
178
- export class RelayHttpError extends Error {
179
- constructor(
180
- readonly method: string,
181
- readonly path: string,
182
- readonly status: number,
183
- readonly statusText: string,
184
- readonly body: string,
185
- ) {
186
- super(`relay ${method} ${path} failed: ${status}`);
187
- }
188
- }
package/start-live.sh DELETED
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- ROOT_DIR=$(cd "$(dirname "$0")/.." && pwd)
5
- CODEX_DIR="$ROOT_DIR/codex"
6
- RUNTIME_DIR="$CODEX_DIR/runtime"
7
- APP_LOG="$RUNTIME_DIR/app-server.log"
8
- APP_PID="$RUNTIME_DIR/app-server.pid"
9
- APP_URL_FILE="$RUNTIME_DIR/app-server.url"
10
- STATE_FILE="${AGENT_RELAY_CODEX_STATE_PATH:-$RUNTIME_DIR/live-state.json}"
11
- CODEX_APP_SERVER_URL="${CODEX_APP_SERVER_URL:-ws://127.0.0.1:4501}"
12
- STARTED_APP_SERVER=0
13
-
14
- mkdir -p "$RUNTIME_DIR"
15
-
16
- cleanup() {
17
- if [ "$STARTED_APP_SERVER" = "1" ] && [ -f "$APP_PID" ]; then
18
- pid=$(cat "$APP_PID" 2>/dev/null || true)
19
- if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
20
- kill "$pid" 2>/dev/null || true
21
- fi
22
- rm -f "$APP_PID"
23
- fi
24
- }
25
- trap cleanup EXIT INT TERM
26
-
27
- if [ -f "$APP_PID" ]; then
28
- pid=$(cat "$APP_PID" 2>/dev/null || true)
29
- if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
30
- echo "app-server already running (pid=$pid) at $(cat "$APP_URL_FILE" 2>/dev/null || echo "$CODEX_APP_SERVER_URL")"
31
- else
32
- rm -f "$APP_PID"
33
- fi
34
- fi
35
-
36
- if [ ! -f "$APP_PID" ]; then
37
- setsid bash -lc "exec codex app-server --listen '$CODEX_APP_SERVER_URL' >>'$APP_LOG' 2>&1 < /dev/null" &
38
- pid=$!
39
- STARTED_APP_SERVER=1
40
- printf '%s\n' "$pid" > "$APP_PID"
41
- printf '%s\n' "$CODEX_APP_SERVER_URL" > "$APP_URL_FILE"
42
-
43
- for _ in $(seq 1 50); do
44
- if grep -qiE 'listening|ws://' "$APP_LOG" 2>/dev/null; then
45
- break
46
- fi
47
- if ! kill -0 "$pid" 2>/dev/null; then
48
- echo "app-server exited early; see $APP_LOG" >&2
49
- exit 1
50
- fi
51
- sleep 0.2
52
- done
53
-
54
- echo "started app-server pid=$pid url=$CODEX_APP_SERVER_URL"
55
- fi
56
-
57
- echo "state file: $STATE_FILE"
58
- echo "connect Codex TUI with: codex --remote $CODEX_APP_SERVER_URL"
59
- echo "if sidecar creates a new thread first, inspect $STATE_FILE for threadId and use: codex resume <threadId> --remote $CODEX_APP_SERVER_URL"
60
-
61
- export CODEX_APP_SERVER_URL
62
- export AGENT_RELAY_CODEX_CWD="${AGENT_RELAY_CODEX_CWD:-$ROOT_DIR}"
63
- export AGENT_RELAY_CODEX_STATE_PATH="$STATE_FILE"
64
- exec bun run "$CODEX_DIR/live-sidecar.ts"