Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 140 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,166 @@
# bullpen
<h1 align="center">bullpen</h1>

A durable agent harness in Rust. A bullpen is a roster of warmed-up relievers
you call in, pull back, and send out again — agents as managed, durable
workers, not fire-and-forget processes.
<p align="center">
<strong>A durable agent harness in Rust.</strong><br>
Agents as managed, resumable workers — not fire-and-forget processes.
</p>

**Status: v0.** Headless vertical slice — a working agent loop (Anthropic,
six workspace tools, SQLite-backed resumable sessions). TUI, durable
subagents, OS sandboxing, and the workflow engine are on the
[roadmap](ARCHITECTURE.md#roadmap).
<p align="center">
<a href="https://github.com/StructuPath/bullpen/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/StructuPath/bullpen/actions/workflows/ci.yml/badge.svg"></a>
<img alt="Rust 1.97" src="https://img.shields.io/badge/rust-1.97-orange?logo=rust&logoColor=white">
<img alt="License MIT" src="https://img.shields.io/badge/license-MIT-blue">
<img alt="Status v0" src="https://img.shields.io/badge/status-v0-yellow">
</p>

## Use
<p align="center">
<img src="docs/media/bullpen-agents.gif" alt="Dispatching three agents from bullpen agents, watching them complete, peeking at one's answer, and seeing the sessions persist" width="100%">
</p>

A bullpen is a roster of warmed-up relievers you call in, pull back, and send
out again. That is the whole thesis: agent runs should survive the process
that started them.

## Why it's different

**Every step is durable the moment it happens.** Sessions live in one SQLite
database (WAL), not in the memory of whatever process launched them. Kill
bullpen mid-run — crash, `kill -9`, power loss — and the next invocation
recovers: interrupted tool calls are marked, the transcript is closed
cleanly, and the session resumes where it stopped.

**No daemon, no supervisor.** A background session is just a detached
`bullpen run` coordinating through the store. The dashboard is a read-and-
dispatch view over that store — close it and the work keeps going, because
nothing was ever supervising it.

**The sandbox is a feature, not a footnote.** `--sandbox` confines writes to
the workspace on every platform and, on macOS, runs shell commands *and their
children* under Seatbelt. `--sandbox-strict` also cuts network.

## Install

```bash
cargo install --path crates/cli
```

Rust 1.97+ (pinned in `rust-toolchain.toml`, so `rustup` fetches the right
one automatically).

## Quickstart

Point it at a provider — any one of these is enough:

# Pick your provider(s):
export ANTHROPIC_API_KEY=sk-ant-... # anthropic (default), streams live
bullpen login openrouter # OpenRouter, official OAuth PKCE
```bash
export ANTHROPIC_API_KEY=sk-ant-... # streams tokens live
bullpen login openrouter # official OAuth PKCE
bullpen login codex # ChatGPT subscription, device-code flow
export GLM_API_KEY=... # GLM (Z.ai), Anthropic-compatible
export KIMI_API_KEY=... # Kimi (Moonshot), Anthropic-compatible
# ...or skip codex login entirely: bullpen borrows a logged-in Codex CLI's
# session (~/.codex/auth.json) read-only.
```

Already have the Codex CLI logged in? bullpen borrows its session from
`~/.codex/auth.json` read-only — nothing to configure, and it never
refreshes that token so it can't invalidate the other tool's login.

Then work:

```bash
cd your-project
bullpen run "find the failing test, explain why it fails"
bullpen run -p codex "..." # ChatGPT subscription
bullpen run -p glm "..." # or -p kimi, -p openrouter
bullpen run --sandbox "refactor X" # confine writes to the workspace (Seatbelt on macOS)
bullpen run -v "..." # show tool activity on stderr
bullpen sessions # list stored sessions
bullpen sessions --json # machine-readable session list
bullpen run -r <id-prefix> "follow-up question" # resumes with the session's provider

# Dispatch and watch many background sessions from one screen:
bullpen run --bg "audit the auth module" # detached; returns immediately
bullpen agents # dashboard: grouped, live, dispatch + peek
bullpen logs <id-prefix> # tail a background session's output
bullpen run "find the failing test and explain why it fails"
bullpen run --sandbox "refactor the retry logic" # confine writes
bullpen run -v "..." # tool activity on stderr
```

Sessions are resumable by id prefix, with the provider they were created
with:

```bash
bullpen sessions # what have I got
bullpen sessions --json # same, machine-readable
bullpen run -r 6ee4acc9 "now write the fix"
```

## Run many at once

```bash
bullpen run --bg "audit the auth module" # detached, returns immediately
bullpen agents # the dashboard in the GIF above
bullpen logs 6ee4acc9 # tail a background session
```

**Agent view** (`bullpen agents`) manages background sessions from one
screen — dispatch, watch them work, peek their output. It's daemonless:
each background session is a detached `bullpen run` that coordinates through
the SQLite store, so they keep running after you close the dashboard, survive
crashes (a dead process shows as Failed and resumes on `run -r`), and appear
in `bullpen sessions` like anything else.

Providers: **anthropic** (true token streaming + prompt caching), **codex**
(ChatGPT subscription), **openrouter**, **glm**, **kimi**. `--sandbox`
confines file writes to the workspace on every platform and, on macOS, runs
shell commands under Seatbelt so arbitrary code can't escape either;
`--sandbox-strict` also cuts network.

Sessions persist in `~/.bullpen/bullpen.db` (SQLite, WAL) as an append-only
entry tree plus an execution log — every step of a run is durable the moment
it happens. Kill bullpen mid-run (crash, `kill -9`, power loss) and the next
invocation recovers: interrupted tool calls get marked, the transcript is
closed cleanly, and the session resumes where it left off.

The same machinery powers **the pen**: the model can delegate bounded tasks
to child agents via the `agent` tool (`inspect` = read-only, `work` = full
tools). Children are ordinary sessions — durable, budgeted, listed by
`bullpen agents` groups sessions by state — **Working** (running, live pid),
**Failed** (running, dead pid — it crashed), **Completed**, **Idle** — and
lets you dispatch from the input line, `Space` to peek at output, `Esc` to
quit. Quitting stops nothing.

## The pen

The model can delegate bounded work to child agents through the `agent`
tool: `inspect` for read-only reconnaissance, `work` for the full toolset.
Children are ordinary sessions — durable, budgeted, listed by
`bullpen sessions`, resumable — with deterministic identities, so a replayed
delegation reattaches to its child instead of running it twice.

## Providers

| Provider | Wire format | Auth | Verified |
|---|---|---|---|
| `anthropic` | Anthropic messages | `ANTHROPIC_API_KEY` | wire-level tests |
| `codex` | OpenAI Responses (SSE) | `bullpen login codex`, or borrow the Codex CLI | live, incl. tools + resume |
| `openrouter` | OpenAI chat-completions | `bullpen login openrouter` or `OPENROUTER_API_KEY` | live, incl. tools |
| `glm` | Anthropic-compatible | `GLM_API_KEY` | config-only |
| `kimi` | Anthropic-compatible | `KIMI_API_KEY` | config-only |

Adapters are organized by wire format rather than vendor, which is why
compatible hosts are configuration instead of code.

Built-in tools: `bash`, `read_file`, `write_file`, `edit_file`, `grep`,
`glob` — plus `agent` when the pen is enabled.

## Where state lives

`~/.bullpen/bullpen.db` — SQLite in WAL mode, holding an append-only entry
tree (the conversation) plus a separate execution log (the orchestration).
Delete every execution record and you still have a complete, valid
conversation.

Reading it while sessions run needs the immutable flag, since WAL databases
can't be opened read-only without their shared-memory file:

```bash
sqlite3 "file:$HOME/.bullpen/bullpen.db?immutable=1" "select id, status from sessions"
```

## Status

**v0.** Honest about what that means:

| | |
|---|---|
| ✅ Shipped | Durable execution + crash recovery · the pen (durable subagents) · write-confinement sandbox with Seatbelt on macOS · agent view (dispatch, peek, live state) · 5 providers |
| 🚧 Next | Interactive attach to a live session · needs-input state · notifications · compaction |
| 📋 Planned | Landlock confinement on Linux · a durable workflow engine (steps in SQLite, resumable from any step) |

Outside `--sandbox`, tools run with the process's full authority. Run it
somewhere you would trust the model to act.

## Design

Start with [ARCHITECTURE.md](ARCHITECTURE.md). The one-sentence version:
a policy-free core loop (`bullpen-agent`) that knows nothing about vendors,
config, or UI, with everything else composed around it at the edge — plus a
single durable store instead of per-process state.
[ARCHITECTURE.md](ARCHITECTURE.md) is the source of truth. The one-sentence
version: a policy-free core loop (`bullpen-agent`) that knows nothing about
vendors, config, or UI, with everything else composed around it at the edge
— plus a single durable store instead of per-process state.

## Develop

```bash
cargo test # all crates
cargo clippy --all-targets
cargo run -p bullpen -- run "hello"
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
```

All three gate CI on Linux and macOS. The demo above is reproducible —
`docs/media/bullpen-agents.tape` drives the real binary through
[VHS](https://github.com/charmbracelet/vhs).

## License

MIT
Binary file added docs/media/bullpen-agents.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions docs/media/bullpen-agents.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Output bullpen-agents.gif
Output bullpen-agents.mp4
Set Shell zsh
Set FontSize 20
Set Width 1300
Set Height 470
Set Padding 18
Set TypingSpeed 40ms

Hide
Type "export PS1='$ ' PATH=/Users/vics/.cargo/bin:$PATH HOME=/tmp/bpdemo/home; cd /tmp/bpdemo/work; clear"
Enter
Sleep 1s
Show

Sleep 700ms
Type "bullpen agents"
Sleep 300ms
Enter
Sleep 1800ms

Type "find the bug in src/retry.rs"
Sleep 300ms
Enter
Sleep 1500ms

Type "count unchecked items in notes.md"
Sleep 300ms
Enter
Sleep 1500ms

Type "what is MAX_ATTEMPTS set to?"
Sleep 300ms
Enter
Sleep 3s

Sleep 27s

Down
Sleep 600ms
Space
Sleep 5s

Escape
Sleep 500ms
Escape
Sleep 800ms

Type "bullpen sessions"
Sleep 300ms
Enter
Sleep 2500ms