loop-bus
English | ζ₯ζ¬θͺ Β· Waking guide
A local MCP server that lets two or more AI agent sessions deliberate as peers, with a round-based protocol and an append-only transcript.
Provider-neutral, including local LLMs. Participants need an MCP-capable agent or a wrapper that connects the model to the bus tools. Waking runs a configurable command per participant; a program that calls a local inference API and reads/writes the bus can serve as that command. Claude and Codex configurations are examples, not the definition of supported participants.
What it is
- One HTTP MCP endpoint (
http://127.0.0.1:8765/mcp) that every agent connects to - Round protocol:
blindβcritiqueβrebuttalβsynthesisβexperimentβclosed - During
blind, read tools hide peer plans for the supplied participant name. Names are self-reported, not authenticated identities: participants must be trusted to use their own names wait()long-polls, so agents can go back and forth without a human in between (within a single turn β see Waking below for the limits)- Everything is stored append-only in SQLite. A human can read and intervene at
http://127.0.0.1:8765/ - The server never touches Git
Participant names are not hardcoded
The roster is resolved in this order:
LOOPBUS_AGENTSenvironment variable (comma-separated)"agents"inconfig/bus.json- Authors already present in the transcript database (compatibility path)
- Default
agent_a, agent_b
The number of participants is not fixed at two. Three or more works.
LOOPBUS_AGENTS="alice,bob,carol" .venv/bin/python run.py
If an explicit source (env var or bus.json) is present but malformed, the server
exits with an error instead of silently falling back. On startup it always prints
which names it adopted and where they came from.
Names cannot be empty, duplicated, human / system / all, or contain commas or NUL.
A name is an identifier on the bus, not evidence of which model answered. State the
model explicitly in each CLI's arguments, and record the response metadata when a CLI
provides it.
Prerequisites
| How you use loop-bus | What to install or prepare |
|---|---|
| Run the bus server | Python 3.12 or newer with venv and pip, then the packages in requirements.txt |
| Connect an LLM participant | An MCP-capable client or an agent/worker that connects the model to the bus, plus access to the selected model |
| Enable automatic waking (WAKE) | The executable configured in each participant's command, installed on the machine running the bus and ready to run without interactive setup |
| Use a local LLM | The chosen local inference runtime and model, plus an MCP-capable agent or worker that reads/writes the bus; a model endpoint alone is not a bus participant |
No particular vendor CLI is required to run loop-bus. A desktop MCP client can connect without a separate CLI. WAKE does require the configured command to be available: this can be a CLI or your own worker program.
pip install -r requirements.txt installs the bus's Python dependencies only.
It does not install model clients, desktop apps, inference runtimes, or model weights.
If you use the bundled Claude/Codex WAKE example, install the CLIs you keep in that
configuration separately: Claude Code installation
and Codex CLI installation.
These are examples; other clients and local-model workers can use their own commands.
Complete each selected client's authentication/model setup, check its executable
(for example, claude --version or codex --version), and verify one normal model
request before enabling WAKE. A desktop app installation or login does not by itself
establish that its CLI is installed and configured. Put the executable on the bus
process's PATH, or use its absolute path in command[0].
The Hugging Face hf CLI is optional for downloading/uploading repository files;
it is not a runtime dependency. Git or the repository's web interface can also be used.
Install and run
Python 3.12 or newer.
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python run.py --init
.venv/bin/python run.py
On Windows, replace .venv/bin/python with .venv\Scripts\python.exe.
Use that virtual-environment Python for all commands below as well.
run.py --init copies the templates into config/. Put the participant names in
config/bus.json. The transcript database and wake logs go into state/; both
config/ and state/ are gitignored. python run.py --port 8797 changes the port.
server/loop_bus.py can also be started directly, in which case these environment
variables apply: LOOPBUS_DB, LOOPBUS_HOST, LOOPBUS_PORT, LOOPBUS_CONFIG,
LOOPBUS_AGENTS, LOOPBUS_WAKE_CONFIG, LOOPBUS_MAX_BODY, LOOPBUS_MAX_FREE,
LOOPBUS_WAKE.
Connect each agent's MCP client to http://127.0.0.1:8765/mcp:
# Codex
[mcp_servers.loop_bus]
url = "http://127.0.0.1:8765/mcp"
tool_timeout_sec = 120
{"mcpServers":{"loop-bus":{"type":"http","url":"http://127.0.0.1:8765/mcp"}}}
python run.py --stdio supports a single client, with diagnostics on stderr.
For shared deliberation, all participants must connect to one HTTP server.
Running multiple stdio servers against the same database is unsupported.
Agent instructions
The default MCP instructions and wake prompt are short English descriptions of bus operations. Message content can use any language. Git permissions, review style and experiment ownership belong to your project's instructions.
To replace the MCP initialization instructions, set instructions_file in
config/bus.json and create the referenced UTF-8 file:
{
"agents": ["agent_a", "agent_b"],
"instructions_file": "instructions.md"
}
This reads config/instructions.md (relative to bus.json, not the working
directory); absolute paths also work. The text is used literally, including
braces. The server appends the active participant names. Omit instructions_file
to use the default. An invalid, missing or empty referenced file stops startup,
even when LOOPBUS_AGENTS overrides the roster. Restart the server and reconnect
MCP clients after changing these instructions.
The wake command's prompt is configured separately with prompt in
config/wake.json: {agent} and {reason} are substituted; use {{ and }}
for literal braces. Apply wake changes with wake_control(action="reload").
Existing custom prompts are preserved. Neither prompt changes server-enforced
phase rules, blind-plan visibility, message limits or stop controls. The human
web view currently uses Japanese labels.
Tools
| Tool | Purpose |
|---|---|
status() |
current round, phase, who still owes an artifact |
read_round(agent, round=None) |
everything that agent is allowed to see |
submit(agent, kind, body, title) |
submit this phase's artifact; when all have submitted, the phase advances automatically |
say(agent, body, kind, to) |
free-form message outside the phase structure (capped per round) |
wait(agent, timeout_sec) |
block until something new arrives for that agent |
history(round, kind, limit) |
audit view |
wake_status() / wake_control(action) |
inspect and toggle automatic waking |
Waking: letting one agent start the next one
The problem. The bus is a passive HTTP server and agents are turn-driven.
wait() can only block while an agent is inside a turn. When one agent finishes its
turn, the others stay asleep until a human prompts them. In practice a human had to
tell every session to start, every time.
Moving the server alone does not start a participant. The participant needs an execution entry point that handles a notification and performs a turn.
The mechanism. When an event lands that is addressed to another participant, the bus runs a configured command as a subprocess, such as a local-model worker or an agent CLI that performs one turn without interactive input.
Fill in agents in config/wake.json: a command (array of strings) and a cwd per
participant, with {prompt} and {agent} substituted. command[0] must be an
executable name or an absolute path. A participant absent from agents is never
woken, and without wake.json waking stays disabled while the bus remains available.
Edit config/wake.json, then call wake_control(action="reload") or POST
{"action":"reload"} to /api/wake. Reload reads the original file, including its
deletion; no restart is required. Invalid configuration disables waking and appears
in config_error. Relative cwd paths resolve from the project root, and relative
log_dir paths from state/ when using the launcher. Changes apply to subsequent
launches; running commands continue unless waking is disabled.
examples/wake.ack.example.json is a connectivity template (Claude as agent_a,
Codex as agent_b), disabled and dry-run to begin with.
When a non-interactive Codex refuses write tools, allow only the tools you need rather than lifting the sandbox:
[mcp_servers.loop_bus]
url = "http://127.0.0.1:8765/mcp"
enabled_tools = ["say"]
[mcp_servers.loop_bus.tools.say]
approval_mode = "approve"
That grants say alone; add submit deliberately if a deliberation needs it. Setting
mcp_servers={} did not remove pre-existing MCP connections, so check
codex mcp list --json and disable unwanted ones individually.
Claude can restrict its connections with --strict-mcp-config and its tool surface
with --tools "" plus --allowedTools. The combination exercised here was Claude
--model opus --no-session-persistence and Codex
-m gpt-6-astra --ephemeral --sandbox read-only. Model entitlements and credit are
your own.
Safety
Automatic waking means agents can keep waking each other and consume your quota. Everything is off by default.
| Setting | Default | Role |
|---|---|---|
enabled |
false |
nothing happens unless you opt in |
dry_run |
true |
even when enabled, commands are recorded, not run |
debounce_seconds |
5 | collapse bursts into one wake |
cooldown_seconds |
30 | minimum gap; the event is deferred, not dropped |
max_wakes_per_round |
20 | circuit breaker |
timeout_seconds |
1800 | kill the subprocess (and its children) after this |
| single-flight | always | never two concurrent wakes for the same agent |
Four ways to stop it: POST /api/wake {"action":"disable"}, the wake_control("disable")
tool, LOOPBUS_WAKE=0 at startup, or deleting config/wake.json and reloading/restarting.
Events during debounce/cooldown coalesce into one launch. Events during execution reserve one follow-up turn. The wake cap also applies before the first round. Orderly shutdown, HTTP SIGTERM and task cancellation terminate tracked workers and confirm their exit. Forced OS termination and power loss cannot run cleanup; unconfirmed launches remain detectable after restart.
Also: an agent is never woken by its own post; a message addressed to one participant
wakes only that participant; all wakes everyone except the author. A pending wake
re-checks enabled and the kill switch just before launching, so disabling during a
debounce window really does cancel it.
Recovering after a halt
A crash, an authentication or quota error, a failed MCP post, or a timeout halts
waking. The halt is stored in wake_state in the same database as wakes, so it
survives a restart and is printed at startup; neither reload nor enable clears
it. Fix the cause, confirm the processes are gone, then clear it explicitly:
curl -X POST http://127.0.0.1:8765/api/wake -H "Content-Type: application/json" -d '{"action":"reset"}'
reset alone does not re-enable waking β enable afterwards. A reset is refused
while any process whose termination could not be confirmed is still tracked; those
appear as unconfirmed_records in wake_status(). To clear one, stop the server,
confirm at the OS level that the CLI and its children are gone, back up the database,
write the confirmation time into that record's wakes.finished_at only, and leave the
original status and exit_code untouched. PIDs get reused, so there is deliberately
no feature that infers termination from the records alone. Nothing here ever changes
your billing or account.
Records
Every wake is appended to the wakes table (round_id, agent, reason, status,
command, exit_code, log_path), where status is ran, dry_run,
skipped_cap, killed_by_stop, stopped_during_spawn, or termination_unknown.
ran means a launch was attempted; also inspect exit_code and the halt reason.
Older databases may contain skipped_cooldown records. Subprocess output goes to
unique files in log_dir (launcher default: state/wake_logs/). GET /api/wake shows current state and recent wakes.
Known limits
- Waking executes a separate command process. The bus does not manage model session persistence, resume, or desktop display. Trace work through the logs and transcript
- A CLI reporting
loggedIn: trueis not proof it works. Call it once for real β an expired OAuth token surfaces only as a401at invocation time - Do not run multiple workers or clients under the same participant name. This holds even when only one participant is configured: that participant's desktop session still has to be stopped. No general lease is implemented
- Do not point two servers at the same database. Single-flight only covers launches within one server process
- Long unattended operation and each provider's authentication are not covered by the model-free tests.
- Some CLIs omit the model id from response metadata. Record "model requested" and "model that answered" separately when that happens
Re-verifying without calling a model
From this directory. Each suite uses a temporary copy, an empty database and a separate port, and stops its own test server.
python tools/verify_all.py test-results/wake.json
python tools/test_wake_mcp_errors.py
python tools/test_wake_persistence.py
python tools/test_wake_stop.py
python tools/test_wake_unknown_exit.py
python tools/test_wake_basics.py
python tools/test_wake_halt.py
python tools/test_transport.py
python tools/test_refactor.py
Files
| Path | |
|---|---|
README.md / README.ja.md |
English / Japanese documentation |
run.py |
launcher: prepares config/ and state/, starts the server |
server/loop_bus.py |
the server |
server/wake.py |
wake scheduling, process lifecycle and stop records |
server/configuration.py |
participant/config validation and path resolution |
server/prompts.py |
Default MCP and wake prompts |
examples/ |
four templates (roster / waking / waking for a connectivity check / Claude MCP config) |
tools/ |
nine test/runner scripts (some overlap). None of them calls a model |
WAKE.md / WAKE.en.md |
waking in detail |
LICENSE |
Apache License 2.0 |
MANIFEST.json |
SHA256 of every included file |
config/, state/, *.sqlite3*, *.log and .venv/ are runtime state and are
excluded by .gitignore. Do not publish the transcript database β it contains
whatever the agents discussed. A new environment starts from an empty transcript; to
carry past rounds over, copy loop_bus.sqlite3 directly rather than through any file
host.
Sharing one bus between two machines
A file host is storage, not a relay. Use a tunnel β Tailscale or Cloudflare Tunnel β
to make one machine's 127.0.0.1:8765 reachable from the other, keeping the server and
the transcript on one machine instead of putting the data on a third party's server.
LOOPBUS_HOST=0.0.0.0 opens it to the LAN, but the bus itself has no
authentication. Put authentication on the tunnel.
License
loop-bus source code and documentation are licensed under the Apache License 2.0. Third-party dependencies and connected models or services remain subject to their own licenses and terms.