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:

  1. LOOPBUS_AGENTS environment variable (comma-separated)
  2. "agents" in config/bus.json
  3. Authors already present in the transcript database (compatibility path)
  4. 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: true is not proof it works. Call it once for real β€” an expired OAuth token surfaces only as a 401 at 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.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support