batonsh 0.1.0__tar.gz

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.
@@ -0,0 +1,13 @@
1
+ node_modules/
2
+ dist/
3
+ .baton/run/
4
+ .baton/state.db*
5
+ .baton/worktrees/
6
+ *.log
7
+ .DS_Store
8
+ coverage/
9
+ dist/
10
+ __pycache__/
11
+ *.pyc
12
+ *.egg-info/
13
+ .venv/
batonsh-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: batonsh
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Baton, the multi-agent coding TUI/daemon
5
+ License: MIT
6
+ Classifier: Operating System :: POSIX
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Typing :: Typed
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Baton Python SDK
13
+
14
+ Drive [Baton](../README.md), the multi-agent coding daemon, from Python. It is a small,
15
+ standard-library-only client for the Baton control protocol (JSON-RPC 2.0 over the project's Unix socket),
16
+ the same API the TUI uses.
17
+
18
+ - Python 3.9 or newer, with no dependencies. Works on macOS and Linux (Unix sockets).
19
+ - Import name `baton`, distribution name `batonsh`.
20
+
21
+ ```sh
22
+ pip install ./sdk-python # or just put sdk-python/ on PYTHONPATH
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ import baton
29
+
30
+ with baton.connect() as client: # finds the project, starts `baton daemon` if needed
31
+ run = client.run_state() # self-updating view of the run
32
+ seq = run.last_seq
33
+ client.submit("migrate the tests to pytest") # same as typing it in the TUI
34
+ run.wait_idle(timeout=900, since_seq=seq) # until every agent is idle again
35
+ for a in run.agents.values():
36
+ print(a.name, a.status, a.diff, "$%.3f" % a.cost, a.last_message)
37
+ ```
38
+
39
+ ## Connecting
40
+
41
+ ```python
42
+ client = baton.connect(
43
+ cwd=None, # any directory inside the project (default: os.getcwd())
44
+ socket_path=None, # explicit socket (skips discovery)
45
+ autostart=True, # launch a detached daemon when nothing answers
46
+ client_name="python",
47
+ daemon_command=None, # ["baton", "daemon", "--root", "{root}"] or lambda root: [...]
48
+ start_timeout=15.0,
49
+ env=None, # extra env vars for a started daemon
50
+ )
51
+ ```
52
+
53
+ `connect()` walks up from `cwd` to the project root (the first directory with `.baton/config.toml`,
54
+ otherwise the nearest `.git`). It connects to `.baton/run/baton.sock`, or to the path stored in
55
+ `.baton/run/socket-path` for deeply nested projects. Without a running daemon it starts
56
+ `baton daemon --root <root>` using `$BATON_BIN` (shell-split, e.g. `BATON_BIN="bun packages/cli/src/main.ts"`)
57
+ or `baton` on `PATH`, waits for the socket, and raises `BatonError` if the daemon exits first.
58
+ The `hello` handshake is done for you: `client.project_root`, `client.run_id`, `client.daemon_pid`.
59
+
60
+ ## Calls
61
+
62
+ Each control method has a wrapper. Results are plain dicts with the protocol's camelCase keys.
63
+
64
+ ```python
65
+ client.snapshot() # agents, tasks, approvals, totals, autonomy, lastSeq
66
+ info = client.spawn("codex", label="review", approval="auto-edit", options={"speed": 0})
67
+ client.prompt(info["agentId"], "review the latest diff", delivery="steer")
68
+ client.submit("@codex-2 verify codex-1's fix") # @mentions, /commands, or a task for the orchestrator
69
+ client.send("*", "freeze: release in 10 minutes", delivery="interrupt")
70
+ client.respond(agent_id, request_id, option_id) # answer a permission request (None cancels)
71
+ client.interrupt(agent_id); client.kill(agent_id)
72
+ client.set_autonomy("auto") # normal | ask | auto | yolo
73
+ client.merge(agent_id); print(client.diff(agent_id))
74
+ client.query_events(types=["tool.*"], limit=50)
75
+ client.call("runs.list", {"limit": 5}) # anything else, by method name
76
+ client.shutdown() # stop the daemon
77
+ ```
78
+
79
+ Errors raise `baton.BatonError`, which carries `.code`, `.message` and `.data`. A closed connection
80
+ raises `baton.ConnectionClosed`, and a `timeout=` on `call()` raises `baton.BatonTimeoutError`, which
81
+ is also a `TimeoutError`.
82
+
83
+ ## Events
84
+
85
+ Every event is a dict `{id, seq, ts, runId, agentId?, type, data}` (see the BEP table in
86
+ [docs/sdk.md](../docs/sdk.md)).
87
+
88
+ ```python
89
+ # iterator: subscribes immediately; breaking out of the loop unsubscribes; ends when the connection closes
90
+ for e in client.events(types=["tool.*", "bus.message"], from_seq=0):
91
+ print(e["seq"], e["type"], e["data"])
92
+
93
+ # callback on a worker thread (it may call the client)
94
+ def approve_reads(e):
95
+ d = e["data"]
96
+ if d.get("kind") == "read":
97
+ allow = next(o["optionId"] for o in d["options"] if o["kind"] == "allow_once")
98
+ client.respond(e["agentId"], d["requestId"], allow)
99
+
100
+ sub = client.on_event(approve_reads, types=["permission.requested"])
101
+ ...
102
+ sub.close()
103
+ ```
104
+
105
+ `events(timeout=…)` raises `TimeoutError` when nothing arrives for that long. `client.subscribe(handler)`
106
+ is the low-level form. Its handler runs on the socket reader thread, so it must be fast and must not
107
+ call the client.
108
+
109
+ ## Run state
110
+
111
+ `client.run_state()` replays the whole run, then follows it live, and gives you:
112
+
113
+ - `agents`: `{agentId: Agent}`, where each `Agent` has `name adapter status label role model cost usage diff tool_calls turns last_message busy alive`
114
+ - `approvals`: `{requestId: Approval}`
115
+ - `tasks`, `workspaces`, `bus`, `autonomy`, `mode`, `totals`, `last_seq`
116
+ - `wait_for(predicate, timeout)`, `wait_idle(timeout, since_seq=None, settle=0.3)`, `is_idle()`, `find_agent(name_or_id)`
117
+
118
+ ```python
119
+ run = client.run_state()
120
+ coder = run.wait_for(lambda s: s.find_agent("codex-1"), timeout=30)
121
+ run.wait_for(lambda s: not s.approvals and coder.status == "ready", timeout=600)
122
+ ```
123
+
124
+ Pass `since_seq=run.last_seq` (captured before you submit work) to `wait_idle` so it does not return
125
+ before the work starts. `settle` requires the run to stay quiet for that long, which covers short idle
126
+ gaps when agents hand off to each other.
127
+
128
+ ## Examples
129
+
130
+ - `examples/watch.py` prints a live one-line-per-event feed (`--replay`, `--types 'tool.*'`, `--agent NAME`).
131
+ - `examples/run_task.py "task" --approve reads` submits a task, waits until every agent is idle, and prints cost, per-agent changes and the files touched.
132
+
133
+ ## Tests
134
+
135
+ ```sh
136
+ python3 -m unittest discover -s sdk-python/tests -v
137
+ ```
138
+
139
+ The integration tests start a throwaway daemon with `bun packages/cli/src/main.ts daemon` in a temporary
140
+ git repo, using mock agents and temporary `BATON_CONFIG_DIR`/`BATON_STATE_DIR`. They are skipped when `bun`
141
+ is not installed.
@@ -0,0 +1,130 @@
1
+ # Baton Python SDK
2
+
3
+ Drive [Baton](../README.md), the multi-agent coding daemon, from Python. It is a small,
4
+ standard-library-only client for the Baton control protocol (JSON-RPC 2.0 over the project's Unix socket),
5
+ the same API the TUI uses.
6
+
7
+ - Python 3.9 or newer, with no dependencies. Works on macOS and Linux (Unix sockets).
8
+ - Import name `baton`, distribution name `batonsh`.
9
+
10
+ ```sh
11
+ pip install ./sdk-python # or just put sdk-python/ on PYTHONPATH
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ ```python
17
+ import baton
18
+
19
+ with baton.connect() as client: # finds the project, starts `baton daemon` if needed
20
+ run = client.run_state() # self-updating view of the run
21
+ seq = run.last_seq
22
+ client.submit("migrate the tests to pytest") # same as typing it in the TUI
23
+ run.wait_idle(timeout=900, since_seq=seq) # until every agent is idle again
24
+ for a in run.agents.values():
25
+ print(a.name, a.status, a.diff, "$%.3f" % a.cost, a.last_message)
26
+ ```
27
+
28
+ ## Connecting
29
+
30
+ ```python
31
+ client = baton.connect(
32
+ cwd=None, # any directory inside the project (default: os.getcwd())
33
+ socket_path=None, # explicit socket (skips discovery)
34
+ autostart=True, # launch a detached daemon when nothing answers
35
+ client_name="python",
36
+ daemon_command=None, # ["baton", "daemon", "--root", "{root}"] or lambda root: [...]
37
+ start_timeout=15.0,
38
+ env=None, # extra env vars for a started daemon
39
+ )
40
+ ```
41
+
42
+ `connect()` walks up from `cwd` to the project root (the first directory with `.baton/config.toml`,
43
+ otherwise the nearest `.git`). It connects to `.baton/run/baton.sock`, or to the path stored in
44
+ `.baton/run/socket-path` for deeply nested projects. Without a running daemon it starts
45
+ `baton daemon --root <root>` using `$BATON_BIN` (shell-split, e.g. `BATON_BIN="bun packages/cli/src/main.ts"`)
46
+ or `baton` on `PATH`, waits for the socket, and raises `BatonError` if the daemon exits first.
47
+ The `hello` handshake is done for you: `client.project_root`, `client.run_id`, `client.daemon_pid`.
48
+
49
+ ## Calls
50
+
51
+ Each control method has a wrapper. Results are plain dicts with the protocol's camelCase keys.
52
+
53
+ ```python
54
+ client.snapshot() # agents, tasks, approvals, totals, autonomy, lastSeq
55
+ info = client.spawn("codex", label="review", approval="auto-edit", options={"speed": 0})
56
+ client.prompt(info["agentId"], "review the latest diff", delivery="steer")
57
+ client.submit("@codex-2 verify codex-1's fix") # @mentions, /commands, or a task for the orchestrator
58
+ client.send("*", "freeze: release in 10 minutes", delivery="interrupt")
59
+ client.respond(agent_id, request_id, option_id) # answer a permission request (None cancels)
60
+ client.interrupt(agent_id); client.kill(agent_id)
61
+ client.set_autonomy("auto") # normal | ask | auto | yolo
62
+ client.merge(agent_id); print(client.diff(agent_id))
63
+ client.query_events(types=["tool.*"], limit=50)
64
+ client.call("runs.list", {"limit": 5}) # anything else, by method name
65
+ client.shutdown() # stop the daemon
66
+ ```
67
+
68
+ Errors raise `baton.BatonError`, which carries `.code`, `.message` and `.data`. A closed connection
69
+ raises `baton.ConnectionClosed`, and a `timeout=` on `call()` raises `baton.BatonTimeoutError`, which
70
+ is also a `TimeoutError`.
71
+
72
+ ## Events
73
+
74
+ Every event is a dict `{id, seq, ts, runId, agentId?, type, data}` (see the BEP table in
75
+ [docs/sdk.md](../docs/sdk.md)).
76
+
77
+ ```python
78
+ # iterator: subscribes immediately; breaking out of the loop unsubscribes; ends when the connection closes
79
+ for e in client.events(types=["tool.*", "bus.message"], from_seq=0):
80
+ print(e["seq"], e["type"], e["data"])
81
+
82
+ # callback on a worker thread (it may call the client)
83
+ def approve_reads(e):
84
+ d = e["data"]
85
+ if d.get("kind") == "read":
86
+ allow = next(o["optionId"] for o in d["options"] if o["kind"] == "allow_once")
87
+ client.respond(e["agentId"], d["requestId"], allow)
88
+
89
+ sub = client.on_event(approve_reads, types=["permission.requested"])
90
+ ...
91
+ sub.close()
92
+ ```
93
+
94
+ `events(timeout=…)` raises `TimeoutError` when nothing arrives for that long. `client.subscribe(handler)`
95
+ is the low-level form. Its handler runs on the socket reader thread, so it must be fast and must not
96
+ call the client.
97
+
98
+ ## Run state
99
+
100
+ `client.run_state()` replays the whole run, then follows it live, and gives you:
101
+
102
+ - `agents`: `{agentId: Agent}`, where each `Agent` has `name adapter status label role model cost usage diff tool_calls turns last_message busy alive`
103
+ - `approvals`: `{requestId: Approval}`
104
+ - `tasks`, `workspaces`, `bus`, `autonomy`, `mode`, `totals`, `last_seq`
105
+ - `wait_for(predicate, timeout)`, `wait_idle(timeout, since_seq=None, settle=0.3)`, `is_idle()`, `find_agent(name_or_id)`
106
+
107
+ ```python
108
+ run = client.run_state()
109
+ coder = run.wait_for(lambda s: s.find_agent("codex-1"), timeout=30)
110
+ run.wait_for(lambda s: not s.approvals and coder.status == "ready", timeout=600)
111
+ ```
112
+
113
+ Pass `since_seq=run.last_seq` (captured before you submit work) to `wait_idle` so it does not return
114
+ before the work starts. `settle` requires the run to stay quiet for that long, which covers short idle
115
+ gaps when agents hand off to each other.
116
+
117
+ ## Examples
118
+
119
+ - `examples/watch.py` prints a live one-line-per-event feed (`--replay`, `--types 'tool.*'`, `--agent NAME`).
120
+ - `examples/run_task.py "task" --approve reads` submits a task, waits until every agent is idle, and prints cost, per-agent changes and the files touched.
121
+
122
+ ## Tests
123
+
124
+ ```sh
125
+ python3 -m unittest discover -s sdk-python/tests -v
126
+ ```
127
+
128
+ The integration tests start a throwaway daemon with `bun packages/cli/src/main.ts daemon` in a temporary
129
+ git repo, using mock agents and temporary `BATON_CONFIG_DIR`/`BATON_STATE_DIR`. They are skipped when `bun`
130
+ is not installed.
@@ -0,0 +1,54 @@
1
+ """Python SDK for Baton — drive the multi-agent daemon (batond) from scripts.
2
+
3
+ import baton
4
+
5
+ with baton.connect() as client: # starts the project daemon if needed
6
+ run = client.run_state()
7
+ seq = run.last_seq
8
+ client.submit("fix the flaky test in tests/api")
9
+ run.wait_idle(timeout=600, since_seq=seq)
10
+ for a in run.agents.values():
11
+ print(a.name, a.status, a.diff, "$%.2f" % a.cost)
12
+ """
13
+
14
+ from ._discovery import discover_socket, find_project_root, project_paths
15
+ from .client import (
16
+ CONTROL_PROTOCOL_VERSION,
17
+ BatonError,
18
+ BatonTimeoutError,
19
+ Client,
20
+ Conflict,
21
+ ConnectionClosed,
22
+ InvalidParams,
23
+ NotFound,
24
+ Subscription,
25
+ Unavailable,
26
+ VersionMismatch,
27
+ connect,
28
+ )
29
+ from .run import BUSY_STATUSES, Agent, Approval, RunState, Workspace
30
+
31
+ __version__ = "0.1.0"
32
+
33
+ __all__ = [
34
+ "CONTROL_PROTOCOL_VERSION",
35
+ "BUSY_STATUSES",
36
+ "Agent",
37
+ "Approval",
38
+ "BatonError",
39
+ "BatonTimeoutError",
40
+ "Client",
41
+ "ConnectionClosed",
42
+ "Conflict",
43
+ "InvalidParams",
44
+ "NotFound",
45
+ "Unavailable",
46
+ "VersionMismatch",
47
+ "RunState",
48
+ "Subscription",
49
+ "Workspace",
50
+ "connect",
51
+ "discover_socket",
52
+ "find_project_root",
53
+ "project_paths",
54
+ ]
@@ -0,0 +1,50 @@
1
+ """Project root and socket discovery (mirrors @batonsh/kernel findProjectRoot / projectPaths)."""
2
+
3
+ import os
4
+ from typing import Dict, Optional
5
+
6
+
7
+ def find_project_root(start: Optional[str] = None) -> str:
8
+ """Walk up from ``start`` looking for ``.baton/config.toml``, then ``.git``; falls back to ``start``."""
9
+ begin = os.path.abspath(start if start is not None else os.getcwd())
10
+ d = begin
11
+ git_root = None
12
+ while True:
13
+ if os.path.exists(os.path.join(d, ".baton", "config.toml")):
14
+ return d
15
+ if git_root is None and os.path.exists(os.path.join(d, ".git")):
16
+ git_root = d
17
+ parent = os.path.dirname(d)
18
+ if parent == d:
19
+ break
20
+ d = parent
21
+ return git_root if git_root is not None else begin
22
+
23
+
24
+ def project_paths(root: str) -> Dict[str, str]:
25
+ d = os.path.join(root, ".baton")
26
+ run = os.path.join(d, "run")
27
+ return {
28
+ "dir": d,
29
+ "config": os.path.join(d, "config.toml"),
30
+ "state": os.path.join(d, "state.db"),
31
+ "run": run,
32
+ "socket": os.path.join(run, "baton.sock"),
33
+ "pid_file": os.path.join(run, "batond.pid"),
34
+ "log": os.path.join(run, "batond.log"),
35
+ "worktrees": os.path.join(d, "worktrees"),
36
+ }
37
+
38
+
39
+ def discover_socket(project_root: str) -> str:
40
+ """The daemon's socket: ``.baton/run/socket-path`` (long project paths) or ``.baton/run/baton.sock``."""
41
+ paths = project_paths(project_root)
42
+ alt = os.path.join(paths["run"], "socket-path")
43
+ try:
44
+ with open(alt, "r", encoding="utf-8") as f:
45
+ p = f.read().strip()
46
+ if p:
47
+ return p
48
+ except OSError:
49
+ pass
50
+ return paths["socket"]