agent-relay-codex 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.
- package/README.md +124 -0
- package/app-client.ts +239 -0
- package/approval.ts +29 -0
- package/bin/agent-relay-codex.ts +988 -0
- package/hooks/session-start-lib.ts +25 -0
- package/hooks/session-start.ts +194 -0
- package/install-codex.ps1 +47 -0
- package/install-codex.sh +75 -0
- package/live-sidecar.ts +685 -0
- package/package.json +48 -0
- package/plugin/.codex-plugin/plugin.json +40 -0
- package/plugin/skills/agent-relay/SKILL.md +29 -0
- package/relay.ts +145 -0
- package/start-live.sh +64 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-relay-codex",
|
|
3
|
+
"version": "0.4.14",
|
|
4
|
+
"description": "Codex integration for Agent Relay — auto-registers sessions as agents and enables inter-agent messaging",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-relay-codex": "bin/agent-relay-codex.ts",
|
|
8
|
+
"codex-relay": "bin/agent-relay-codex.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/**/*.ts",
|
|
12
|
+
"hooks/**/*.ts",
|
|
13
|
+
"plugin/**",
|
|
14
|
+
"*.ts",
|
|
15
|
+
"*.sh",
|
|
16
|
+
"*.ps1",
|
|
17
|
+
"!**/*.test.ts",
|
|
18
|
+
"!smoke/**",
|
|
19
|
+
"!runtime/**"
|
|
20
|
+
],
|
|
21
|
+
"license": "AGPL-3.0-or-later",
|
|
22
|
+
"author": "Edin Mujkanovic <edin@exelerus.com>",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/edimuj/agent-relay.git",
|
|
26
|
+
"directory": "codex"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/edimuj/agent-relay/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/edimuj/agent-relay/tree/main/codex#readme",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"codex",
|
|
34
|
+
"codex-plugin",
|
|
35
|
+
"agent-relay",
|
|
36
|
+
"ai-agents",
|
|
37
|
+
"coding-agents",
|
|
38
|
+
"multi-agent",
|
|
39
|
+
"inter-agent-communication",
|
|
40
|
+
"agent-communication",
|
|
41
|
+
"message-bus",
|
|
42
|
+
"automation",
|
|
43
|
+
"developer-tools"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"bun": ">=1.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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.
|
package/relay.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
subject?: string;
|
|
10
|
+
body: string;
|
|
11
|
+
replyTo?: number;
|
|
12
|
+
claimable?: boolean;
|
|
13
|
+
type?: string;
|
|
14
|
+
meta?: Record<string, unknown>;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RelayConfig {
|
|
19
|
+
relayUrl: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
rig: string;
|
|
22
|
+
capabilities: string[];
|
|
23
|
+
tags: string[];
|
|
24
|
+
threadId: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
appServerUrl: string;
|
|
27
|
+
approvalMode?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildAgentIdentity(config: RelayConfig): { id: string; name: string; machine: string; project: string } {
|
|
31
|
+
const machine = hostname();
|
|
32
|
+
const project = config.cwd.split("/").filter(Boolean).at(-1) || "unknown";
|
|
33
|
+
const shortThread = config.threadId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "thread";
|
|
34
|
+
const id = `${machine}-${config.rig}-${project}-${shortThread}`;
|
|
35
|
+
const name = `${project} (${config.rig} @ ${machine})`;
|
|
36
|
+
return { id, name, machine, project };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class RelayClient {
|
|
40
|
+
constructor(private readonly baseUrl: string, private readonly log: (msg: string) => void = () => {}) {}
|
|
41
|
+
|
|
42
|
+
async registerAgent(config: RelayConfig): Promise<{ agentId: string; project: string }> {
|
|
43
|
+
const identity = buildAgentIdentity(config);
|
|
44
|
+
const payload = {
|
|
45
|
+
id: identity.id,
|
|
46
|
+
name: identity.name,
|
|
47
|
+
machine: identity.machine,
|
|
48
|
+
rig: config.rig,
|
|
49
|
+
tags: config.tags,
|
|
50
|
+
capabilities: config.capabilities,
|
|
51
|
+
status: "online",
|
|
52
|
+
meta: {
|
|
53
|
+
client: "codex-live",
|
|
54
|
+
threadId: config.threadId,
|
|
55
|
+
appServerUrl: config.appServerUrl,
|
|
56
|
+
cwd: config.cwd,
|
|
57
|
+
model: config.model || null,
|
|
58
|
+
approvalMode: config.approvalMode || "open",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await this.json("POST", "/api/agents", payload);
|
|
63
|
+
return { agentId: identity.id, project: identity.project };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async heartbeat(agentId: string): Promise<void> {
|
|
67
|
+
await this.json("POST", `/api/agents/${encodeURIComponent(agentId)}/heartbeat`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async getCursor(): Promise<number> {
|
|
71
|
+
const result = await this.json("GET", "/api/messages/cursor") as { latestId?: unknown } | null;
|
|
72
|
+
return typeof result?.latestId === "number" && Number.isSafeInteger(result.latestId) ? result.latestId : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async setStatus(agentId: string, status: RelayAgentStatus): Promise<void> {
|
|
76
|
+
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/status`, { status });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async setReady(agentId: string, ready: boolean): Promise<void> {
|
|
80
|
+
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/ready`, { ready });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async pollMessages(agentId: string, sinceId: number): Promise<RelayMessage[]> {
|
|
84
|
+
const url = new URL(`/api/messages`, this.baseUrl);
|
|
85
|
+
url.searchParams.set("for", agentId);
|
|
86
|
+
url.searchParams.set("unread", "true");
|
|
87
|
+
if (sinceId > 0) url.searchParams.set("sinceId", String(sinceId));
|
|
88
|
+
const response = await fetch(url, { headers: this.headers() });
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new RelayHttpError("GET", "/api/messages", response.status, response.statusText, await response.text());
|
|
91
|
+
}
|
|
92
|
+
return (await response.json()) as RelayMessage[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async claimMessage(messageId: number, agentId: string): Promise<boolean> {
|
|
96
|
+
const response = await fetch(new URL(`/api/messages/${messageId}/claim`, this.baseUrl), {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
99
|
+
body: JSON.stringify({ agentId }),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (response.ok) return true;
|
|
103
|
+
if (response.status === 409) return false;
|
|
104
|
+
if (response.status === 400 || response.status === 404) return false;
|
|
105
|
+
throw new RelayHttpError("POST", `/api/messages/${messageId}/claim`, response.status, response.statusText, await response.text());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async markRead(messageId: number, agentId: string): Promise<void> {
|
|
109
|
+
await this.json("PATCH", `/api/messages/${messageId}`, { readBy: agentId });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async json(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
113
|
+
const response = await fetch(new URL(path, this.baseUrl), {
|
|
114
|
+
method,
|
|
115
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
116
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const text = await response.text();
|
|
121
|
+
this.log(`relay error ${method} ${path}: ${response.status} ${text}`);
|
|
122
|
+
throw new RelayHttpError(method, path, response.status, response.statusText, text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (response.status === 204) return null;
|
|
126
|
+
return response.json().catch(() => null);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private headers(base: Record<string, string> = {}): Record<string, string> {
|
|
130
|
+
const token = process.env.AGENT_RELAY_TOKEN;
|
|
131
|
+
return token ? { ...base, "X-Agent-Relay-Token": token } : base;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class RelayHttpError extends Error {
|
|
136
|
+
constructor(
|
|
137
|
+
readonly method: string,
|
|
138
|
+
readonly path: string,
|
|
139
|
+
readonly status: number,
|
|
140
|
+
readonly statusText: string,
|
|
141
|
+
readonly body: string,
|
|
142
|
+
) {
|
|
143
|
+
super(`relay ${method} ${path} failed: ${status}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/start-live.sh
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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"
|