Agent Relay
Coordinate a leader and follower coding session through bounded handoffs, a local message bus, and explicit stops for human decisions.
0.1.0Add to Favorites
Source
Get it from source
Spark does not host a copy of it.
Open sourceReports
Agent outcome reports
No reports yet
Overview
Agent Relay coordinates a leader and follower coding session using a local message bus and a foreground controller. It records handoffs and results while enforcing turn budgets, timeouts, and human decision boundaries. Use it for a bounded plan, build, and review exchange between Codex and Claude Code sessions. Check its client permission and session handover limits before connecting existing work.
What it does
Agent Relay coordinates two coding sessions as a leader and follower, passing work between clients while recording the conversation locally. It supports Claude Code and Codex through adapters. A foreground controller keeps exactly one turn in flight and stops when a human decision is required. The arrangement supports a plan, build, and review loop, or a second opinion on a proposed change.
The durable bus stores messages and events on disk. Adapters expose explicit capabilities, acknowledge receipt, and report completion; they do not invent acknowledgements. Each turn ends with a small relay block that tells the controller what should happen next. The recorded thread and hash-chained trail let you inspect how work moved between sessions and rebuild state from disk.
When to use - and when NOT to
Use Agent Relay to coordinate a bounded task between two coding clients while retaining a place to pause, answer questions, and inspect results. You can set a handoff budget and a per-turn timeout, then monitor the pair through terminal or web consoles. A budget of eight turns counts handoffs, not the operations performed inside each turn.
The relay observes acknowledgements and completion from outside the clients. It does not control a client's internal tool loops, commands, or subagents. A per-turn timeout is the hard limit on that inner work. Existing terminal sessions retain their permissions until handover. Open Claude Code terminal sessions cannot be driven directly: they must be closed and resumed through handover. Attachment to an existing Codex session and handover of existing sessions are opt-in and have not been verified live.
Inputs and outputs
You supply the participating sessions, their leader and follower roles, the task and scope, and execution limits. The controller dispatches one turn at a time and records messages, acknowledgements, results, and human boundaries. Pausing allows an in-flight result to be recorded and the next action queued without dispatching it; resuming continues the exchange. Detaching the team leaves its sessions running.
The demos run on macOS with Python 3.11 and pretend clients, without consuming model quota. A real pair starts with a relay root and participant configuration:
export AGENT_RELAY_ROOT=~/relay-root
bin/relay init --participants codex claude
bin/relay console # [n] launch Codex and Claude Code, [p] pair them, [s] start
Integrations
Claude Code and Codex connect through client adapters. For sessions the relay launches, Claude followers can run without tools or with a coding policy limited to Read, Edit, Write, Glob, and Grep inside a macOS sandbox. That coding policy excludes commands and subagents. Codex multi-agent mode is recorded rather than enforced.
The web console binds to loopback; remote access requires your own tunnel. Webhook and SMS notifications remain off until configured. Read-only discovery of running sessions is available on macOS. Agent Relay is MIT licensed.
Source README
agent-relay
Let AI coding sessions from different vendors work together, while you stay in charge.

Why
Claude can hand work to Claude on its own. But when you work with sessions from different vendors, such as Codex for planning and review and Claude Code for the code, they can't talk to each other. Without a relay, you're the one transferring context: copying plans, results and reviews between terminals.
agent-relay carries the turns for you. One session leads, the other follows, and the relay passes each turn across, records it on your disk, and stops whenever something needs your decision. It's tested with Claude Code and Codex, and you can extend it to whatever agents you work with by writing an adapter.


Try it in two minutes
These demos use pretend sessions, so they're safe to run anywhere and use no model quota. You need macOS and Python 3.11 or newer.
git clone https://github.com/saranyan/agent-relay.git && cd agent-relay
scripts/console-demo.sh # the web console, with a few pretend sessions
scripts/walkthrough.sh # the full two-terminal flow in a temporary folder
With the Claude Code and Codex CLIs installed, run it on real sessions:
export AGENT_RELAY_ROOT=~/relay-root
bin/relay init --participants codex claude
bin/relay console # [n] launch Codex and Claude Code, [p] pair them, [s] start
The web console tour walks through every screen. The walkthrough shows a recorded run with real message and turn IDs.
What it can and can't see
The relay coordinates two sessions per team, one turn at a time. It sees each turn from the outside: the turn goes in, the client acknowledges it, and later the client reports it finished. What a session does inside a turn is invisible to the relay. That includes tool loops, subagents and long-running commands.
- The turn budget counts hand-offs, not work. One turn can hide a lot of activity. The per-turn timeout is the only hard limit inside a turn.
- The routes the relay starts are locked down where it can. A Claude Code follower starts with no tools at all, or, under the coding policy, with only Read, Edit, Write, Glob and Grep inside a macOS sandbox. It can't start subagents or run commands. Codex's multi-agent mode is recorded but not yet enforced.
- Sessions you open in a terminal keep whatever you allowed them until you hand them over.
The full list of guarantees and limits is in the reference.
What you can use it for
Today, with the prototype:
- Plan, build, review. Codex writes the plan and reviews the result. Claude implements. The loop runs until the reviewer accepts or asks you something.
- A second opinion on every change. Use one model to critique the other's work before you ever see it, so you review a reviewed change.
- Bounded jobs you don't have to babysit. "Fix the failing tests in this folder, at most 8 turns, 15 minutes per turn." Walk away. A decision notification (webhook or SMS, off until you set it up) tells you when it needs you.
- Check in at a glance. The web console works down to phone width. It listens on loopback only, so reaching it from another device takes your own tunnel. The relay deliberately doesn't provide one.
- An audit trail of agent work. Every message and decision is a durable,
hash-chained record that you can replay with
relay threador rebuild from disk.
Ideas the design leaves room for (see the roadmap):
- One manager, many workers. A leader hands pieces of a migration to several followers, one per module, with review between them.
- Other agents. The adapter contract in docs/PROTOCOL.md isn't specific to Claude or Codex. Any coding agent that confirms receipt of a turn and reports completion could plug in.
- Saved playbooks. Reusable goal, scope, check and budget templates, such as "review this branch", "write the docs and fact-check them" or "upgrade this dependency".
- Custom manager styles. Write your own personality next to Reviewer, Planner and Pair programmer, as a plain text file.
- A testbed for agent-to-agent protocols. The bus is a small, durable, inspectable message layer. You can use it to study how agents hand work to each other, where loops stall, and when humans need to step in.
- Evaluation runs. Pit model pairings against the same task with the same budget, and compare the recorded threads.
- Cost and budget insights. Track tokens and time per team, not just turns.
How it works, briefly
you ──start / pause / answer──┐
▼
┌──────────────┐ handoff ┌────────────┐ handoff ┌──────────────┐
│ leader │ ◀──────── │ controller │ ────────▶ │ follower │
│ (Codex) │ ────────▶ │ (fg only) │ ◀──────── │ (Claude) │
└──────────────┘ result └─────┬──────┘ result └──────────────┘
│
durable bus on disk
- Bus: an on-disk store of messages and events.
- Adapters: they talk to each client. Every capability is explicit, and adapters never invent acknowledgments.
- Controller: it runs only in the foreground and keeps exactly one turn in flight. It enforces the budget and the timeout, and stops at every human boundary.
Each turn ends with a small relay block that tells the controller what to do
next. Commands, settings and exit codes are in the reference.
The specification is in docs/PROTOCOL.md.
Status
| Part | State |
|---|---|
| Bus, controller, terminal console, web console | Working, covered by the test suite with pretend clients |
| Discovery of running Claude Code and Codex sessions | Working, read-only, on macOS |
| Codex and Claude Code driving each other (sessions the relay starts) | Verified live: Codex 0.163.0 led Claude Code 2.1.310, 2026-10-25 |
| Attaching to an open Codex session, handing an existing session over | Built, not yet verified live. Off unless you opt in. |
| Driving a Claude Code session while it's open in a terminal | Not possible. Open sessions are handed over (closed and resumed) instead. |
What's needed to get from prototype to dependable is in the roadmap. The details on each client are in integration feasibility.
Documentation
| Document | What it covers |
|---|---|
| Web console tour | Every screen of relay serve, with screenshots |
| Reference | Install, consoles, CLI, configuration, guarantees and limits |
| Walkthrough | A recorded two-terminal run |
| Protocol | The specification: root layout, envelopes, state machines, adapter contract |
| Integration feasibility | What real Claude Code and Codex surfaces can and can't do |
| Context reporting | How session titles and summaries are found |
| Roadmap | What's left, and what could come next |
| Changelog | Release notes |
scripts/console-demo.sh
#!/bin/bash
# Fake-session demo of the relay console. Writes only under $DEMO; no real Claude/Codex
# session and no model is used — "new" managers are pretend sessions too.
# Ctrl-C (or `pkill -f console-demo.sh`) stops the console and every fake session.
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
DEMO="${DEMO:-$(mktemp -d -t relay-console)}"
PORT="${PORT:-47811}"
export AGENT_RELAY_ROOT="$DEMO/root" AGENT_RELAY_FAKE_HOME="$DEMO/home" PYTHONDONTWRITEBYTECODE=1
# Notifications in the demo are written to a local file, never sent.
export AGENT_RELAY_NOTIFY_CONFIG="$DEMO/notify.json" AGENT_RELAY_NOTIFY_DEMO="$DEMO/notify-demo.log"
R="$HERE/bin/relay"
mkdir -p "$DEMO/home"
$R init --participants '*' >/dev/null
PIDS=(); SERVE=""
cleanup() { kill $SERVE "${PIDS[@]}" 2>/dev/null || true; }
trap cleanup EXIT
trap 'exit 130' INT TERM
fake() { # fake <id> <folder> <client> <default-json> [summary] [script-json] [title] [interactive]
mkdir -p "$DEMO/projects/$2"
PYTHONPATH="$HERE" python3 -m relay.adapters.fake_client --home "$DEMO/home" --session-id "$1" \
--cwd "$DEMO/projects/$2" --client "$3" --default "$4" --summary "${5:-}" \
--script "${6:-[]}" --title "${7:-}" --seed-history 3 ${8:+--interactive} >/dev/null 2>&1 &
PIDS+=($!)
}
# Pretend sessions take a realistic while per turn, so there is time to watch and step in.
fake s-payments payments-api Claude '{"natural":true,"sleep":15}' "Payment client timeouts"
fake s-billing billing-service Claude '{"natural":true,"sleep":45}'
fake s-docs docs-site Claude '{"natural":true,"sleep":4}' "" \
'[{"action":"ask_human","subject":"Which docs should change?","text":"I found the retry behaviour in two places: the API reference and the getting-started tutorial. Should I update both, or only the API reference?"}]'
fake s-web web-app Codex '{"natural":true,"sleep":3600}' "Checkout page redesign"
fake s-infra infra-scripts Codex '{"natural":true,"sleep":15}'
# Two pretend sessions in the same folder doing different jobs (simulated, not real sessions):
# the Codex one's summary comes from its (pretend) client metadata, the Claude one's from a manual edit below.
fake s-relay-cx agent-relay Codex '{"natural":true,"sleep":15}' \
"Architecture and review of session management" '[]' "Session manager review"
fake s-relay-cl agent-relay Claude '{"natural":true,"sleep":15}' "" '[]' "Dashboard work"
# Two pretend interactive terminals in one folder: they can't be driven while open, only
# handed over (close them in their terminal; here: stop their process) and taken back.
fake s-mobile-cl mobile-app Claude '{"natural":true,"sleep":3}' "" '[]' "Offline sync" interactive
fake s-mobile-cx mobile-app Codex '{"natural":true,"sleep":2,"finish_after":50}' "" '[]' "Sync review" interactive
sleep 1
echo "busy" > "$DEMO/home/sessions/s-web/manual/now.txt" # web-app is busy with you
$R serve --adapter fake --new-sessions fake --discovery-interval 2 --port "$PORT" 2> "$DEMO/serve.log" & SERVE=$!
for _ in $(seq 50); do grep -q "open:" "$DEMO/serve.log" 2>/dev/null && break; sleep 0.2; done
URL=$(grep -o 'http://[^ ]*' "$DEMO/serve.log" | head -1)
TOKEN=${URL##*t=}
sleep 3 # let discovery find the sessions
# billing-service is already managed by an AI Reviewer, started through the console's own API.
KEY=$(curl -s -H "X-Relay-Token: $TOKEN" "http://127.0.0.1:$PORT/api/sessions" \
| python3 -c 'import json,sys; print(next(s["key"] for s in json.load(sys.stdin)["sessions"] if s["name"]=="billing-service"))')
curl -s -o /dev/null -X POST "http://127.0.0.1:$PORT/api/manage" -H "X-Relay-Token: $TOKEN" \
-H "Origin: http://127.0.0.1:$PORT" -H "Content-Type: application/json" \
-d "{\"key\": \"$KEY\", \"manager\": {\"new\": {\"client\": \"claude\", \"personality\": \"reviewer\"}},
\"goal\": \"Add retries with backoff to the billing API client, with tests for the failure case.\"}"
# docs-site is managed by a Planner and soon stops to ask you a question — it waits until you answer.
KEY=$(curl -s -H "X-Relay-Token: $TOKEN" "http://127.0.0.1:$PORT/api/sessions" \
| python3 -c 'import json,sys; print(next(s["key"] for s in json.load(sys.stdin)["sessions"] if s["name"]=="docs-site"))')
curl -s -o /dev/null -X POST "http://127.0.0.1:$PORT/api/manage" -H "X-Relay-Token: $TOKEN" \
-H "Origin: http://127.0.0.1:$PORT" -H "Content-Type: application/json" \
-d "{\"key\": \"$KEY\", \"manager\": {\"new\": {\"client\": \"codex\", \"personality\": \"planner\"}},
\"goal\": \"Document the new retry behaviour.\"}"
# A manual summary for the Claude session in agent-relay (what you'd type under Details → Edit).
CKEY=$(curl -s -H "X-Relay-Token: $TOKEN" "http://127.0.0.1:$PORT/api/sessions" \
| python3 -c 'import json,sys; print(next(s["context_key"] for s in json.load(sys.stdin)["sessions"] if s["cwd"].endswith("/agent-relay") and s["client"]=="Claude"))')
curl -s -o /dev/null -X POST "http://127.0.0.1:$PORT/api/context" -H "X-Relay-Token: $TOKEN" \
-H "Origin: http://127.0.0.1:$PORT" -H "Content-Type: application/json" \
-d "{\"key\": \"$CKEY\", \"summary\": \"Implementing the dashboard and control refinements\"}"
echo "open: $URL"
echo "demo folder: $DEMO"
wait $SERVE
scripts/walkthrough.sh
#!/bin/bash
# Reproducible fake-client walkthrough. Writes only under $DEMO (default: a new
# temporary directory). Uses no real Claude/Codex session and no model.
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
DEMO="${DEMO:-$(mktemp -d -t relay-demo)}"
export AGENT_RELAY_ROOT="$DEMO/root" AGENT_RELAY_FAKE_HOME="$DEMO/home" PYTHONDONTWRITEBYTECODE=1
export R="$HERE/bin/relay"
j() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; }
say() { printf '\n$ %s\n' "$*"; }
mkdir -p "$DEMO/home" "$DEMO/workspace"
touch "$DEMO/.start-marker"; sleep 1
say relay init --participants codex claude
$R init --participants codex claude
# Two independent fake "terminal" sessions that already have context of their own.
PYTHONPATH="$HERE" python3 -m relay.adapters.fake_client --home "$DEMO/home" --session-id codex-term-1 \
--cwd "$DEMO/workspace" --seed-history 4 --default '{"finish_after":3}' >/dev/null 2>&1 &
CODEX_PID=$!
PYTHONPATH="$HERE" python3 -m relay.adapters.fake_client --home "$DEMO/home" --session-id claude-term-1 \
--cwd "$DEMO/workspace" --seed-history 2 --script '[{}, {"sleep": 3}]' >/dev/null 2>&1 &
CLAUDE_PID=$!
trap 'kill $CODEX_PID $CLAUDE_PID 2>/dev/null || true' EXIT
sleep 1
say relay sessions list --adapter fake
$R sessions list --adapter fake
say relay sessions attach --adapter fake --session codex-term-1 --as codex
L=$($R sessions attach --adapter fake --session codex-term-1 --as codex --json | j '["id"]')
echo "leader endpoint $L"
say relay sessions attach --adapter fake --session claude-term-1 --as claude
F=$($R sessions attach --adapter fake --session claude-term-1 --as claude --json | j '["id"]')
echo "follower endpoint $F"
say relay pair create --leader "$L" --follower "$F" --scope "demo workspace only" --max-turns 10
P=$($R pair create --leader "$L" --follower "$F" --scope "demo workspace only; no network" \
--max-turns 10 --turn-timeout 120 --json | j '["id"]')
$R pair show "$P"
printf 'Implement step 1 of the demo task and report files/tests.\n' > "$DEMO/handoff.md"
say relay pair start "$P" --body-file handoff.md --yes --no-run
$R pair start "$P" --body-file "$DEMO/handoff.md" --yes --no-run 2>/dev/null
say "relay pair run $P (foreground controller, run here in the background for the demo)"
$R pair run "$P" --max-seconds 60 2> "$DEMO/controller.log" > "$DEMO/controller.out" &
CTL=$!
# Pause while the follower's second turn (3 s) is in flight.
python3 - "$P" <<'EOF'
import json, subprocess, sys, time, os
p = sys.argv[1]
while True:
s = json.loads(subprocess.run([os.environ["R"], "pair", "show", p, "--json"], capture_output=True,
text=True).stdout)
if s["turns_used"] == 3 and s["inflight"] and s["inflight"]["phase"] == "accepted":
break
time.sleep(0.05)
EOF
say relay pair pause "$P" --reason "Human review"
$R pair pause "$P" --reason "Human review"
sleep 4
say "relay pair show $P (in-flight result recorded, next handoff queued, nothing dispatched)"
$R pair show "$P"
say relay pair resume "$P"
$R pair resume "$P"
wait $CTL || true
echo "--- controller log ---"; cat "$DEMO/controller.log"
say "(controller has exited: restart the CLI and inspect durable history)"
say relay status
$R status
T=$($R pair show "$P" --json | j '["thread_id"]')
say relay thread "$T"
$R thread "$T"
say relay pair detach "$P"
$R pair detach "$P"
kill -0 $CODEX_PID && kill -0 $CLAUDE_PID && echo "both attached sessions are still running (never closed by the relay)"
say "fake session contexts (pre-existing turns preserved, no replay)"
for s in codex-term-1 claude-term-1; do
python3 -c "import json;c=json.load(open('$DEMO/home/sessions/$s/context.json'));print('$s', 'turns', len(c['turns']), 'routed', c['routed'], 'pre-existing', sum(1 for t in c['turns'] if t['request_id'] is None))"
done
say "files changed outside the demo root since start (expect none)"
find "$HERE" -newer "$DEMO/.start-marker" -type f -not -path '*/__pycache__/*' \
-not -name 'WALKTHROUGH.md' | sed 's/^/ changed: /' || true
echo " (end of list)"
echo "demo directory: $DEMO"
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.