Baton Python SDK
Drive Baton, the multi-agent coding daemon, from Python. It is a small, standard-library-only client for the Baton control protocol (JSON-RPC 2.0 over the project's Unix socket), the same API the TUI uses.
- Python 3.9 or newer, with no dependencies. Works on macOS and Linux (Unix sockets).
- Import name
baton, distribution namebatonsh.
pip install ./sdk-python # or just put sdk-python/ on PYTHONPATH
Quick start
import baton
with baton.connect() as client: # finds the project, starts `baton daemon` if needed
run = client.run_state() # self-updating view of the run
seq = run.last_seq
client.submit("migrate the tests to pytest") # same as typing it in the TUI
run.wait_idle(timeout=900, since_seq=seq) # until every agent is idle again
for a in run.agents.values():
print(a.name, a.status, a.diff, "$%.3f" % a.cost, a.last_message)
Connecting
client = baton.connect(
cwd=None, # any directory inside the project (default: os.getcwd())
socket_path=None, # explicit socket (skips discovery)
autostart=True, # launch a detached daemon when nothing answers
client_name="python",
daemon_command=None, # ["baton", "daemon", "--root", "{root}"] or lambda root: [...]
start_timeout=15.0,
env=None, # extra env vars for a started daemon
)
connect() walks up from cwd to the project root (the first directory with .baton/config.toml,
otherwise the nearest .git). It connects to .baton/run/baton.sock, or to the path stored in
.baton/run/socket-path for deeply nested projects. Without a running daemon it starts
baton daemon --root <root> using $BATON_BIN (shell-split, e.g. BATON_BIN="bun packages/cli/src/main.ts")
or baton on PATH, waits for the socket, and raises BatonError if the daemon exits first.
The hello handshake is done for you: client.project_root, client.run_id, client.daemon_pid.
Calls
Each control method has a wrapper. Results are plain dicts with the protocol's camelCase keys.
client.snapshot() # agents, tasks, approvals, totals, autonomy, lastSeq
info = client.spawn("codex", label="review", approval="auto-edit", options={"speed": 0})
client.prompt(info["agentId"], "review the latest diff", delivery="steer")
client.submit("@codex-2 verify codex-1's fix") # @mentions, /commands, or a task for the orchestrator
client.send("*", "freeze: release in 10 minutes", delivery="interrupt")
client.respond(agent_id, request_id, option_id) # answer a permission request (None cancels)
client.interrupt(agent_id); client.kill(agent_id)
client.set_autonomy("auto") # normal | ask | auto | yolo
client.merge(agent_id); print(client.diff(agent_id))
client.query_events(types=["tool.*"], limit=50)
client.call("runs.list", {"limit": 5}) # anything else, by method name
client.shutdown() # stop the daemon
Errors raise baton.BatonError, which carries .code, .message and .data. A closed connection
raises baton.ConnectionClosed, and a timeout= on call() raises baton.BatonTimeoutError, which
is also a TimeoutError.
Events
Every event is a dict {id, seq, ts, runId, agentId?, type, data} (see the BEP table in
docs/sdk.md).
# iterator: subscribes immediately; breaking out of the loop unsubscribes; ends when the connection closes
for e in client.events(types=["tool.*", "bus.message"], from_seq=0):
print(e["seq"], e["type"], e["data"])
# callback on a worker thread (it may call the client)
def approve_reads(e):
d = e["data"]
if d.get("kind") == "read":
allow = next(o["optionId"] for o in d["options"] if o["kind"] == "allow_once")
client.respond(e["agentId"], d["requestId"], allow)
sub = client.on_event(approve_reads, types=["permission.requested"])
...
sub.close()
events(timeout=…) raises TimeoutError when nothing arrives for that long. client.subscribe(handler)
is the low-level form. Its handler runs on the socket reader thread, so it must be fast and must not
call the client.
Run state
client.run_state() replays the whole run, then follows it live, and gives you:
agents:{agentId: Agent}, where eachAgenthasname adapter status label role model cost usage diff tool_calls turns last_message busy aliveapprovals:{requestId: Approval}tasks,workspaces,bus,autonomy,mode,totals,last_seqwait_for(predicate, timeout),wait_idle(timeout, since_seq=None, settle=0.3),is_idle(),find_agent(name_or_id)
run = client.run_state()
coder = run.wait_for(lambda s: s.find_agent("codex-1"), timeout=30)
run.wait_for(lambda s: not s.approvals and coder.status == "ready", timeout=600)
Pass since_seq=run.last_seq (captured before you submit work) to wait_idle so it does not return
before the work starts. settle requires the run to stay quiet for that long, which covers short idle
gaps when agents hand off to each other.
Examples
examples/watch.pyprints a live one-line-per-event feed (--replay,--types 'tool.*',--agent NAME).examples/run_task.py "task" --approve readssubmits a task, waits until every agent is idle, and prints cost, per-agent changes and the files touched.
Tests
python3 -m unittest discover -s sdk-python/tests -v
The integration tests start a throwaway daemon with bun packages/cli/src/main.ts daemon in a temporary
git repo, using mock agents and temporary BATON_CONFIG_DIR/BATON_STATE_DIR. They are skipped when bun
is not installed.
Metadata
Release files for batonsh 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| batonsh-0.1.0.tar.gz | 23.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| batonsh-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.3 kB
Release files / batonsh-0.1.0.tar.gz
| Download URL | batonsh-0.1.0.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4999883718d75bdf8288418aaa8830aa4f28f2cce91c54ffa072c2e7d9e724b5
|
|
BLAKE2b-256 checksum How to use checksums |
7153beccc0cc1c7eb592d1bc65c6a9dddf99fc62c8c393704522b1497a268b9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / batonsh-0.1.0-py3-none-any.whl
| Download URL | batonsh-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
04b2dab607c3b41cbb39b76027e2c7e3121cbea0799d3309a95d3fc91af8c382
|
|
BLAKE2b-256 checksum How to use checksums |
8b0c4d5ee6569b2594d2d581f5e34bc93bfc4a074d5c0c42d3a9582f708113fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log