A computer-use agent for macOS, Windows, and Linux where BAML is the brain and Python only does what BAML can't: take screenshots and move the mouse and keyboard. It runs on local open-source vision models through LM Studio by default, or on Claude. A live dashboard, built on baml query, shows every step: the screenshot the model saw, every option it considered, the one it chose, and exactly where it clicked.
uv run server.py # http://127.0.0.1:8787, then type a goal
Python (agent.py) BAML (baml_src/)
───────────────── ─────────────────────────────────────────────
look(path) -> Screenshot ◀──┐ run_task(goal, look, act, ask, inbox)
act(op) ◀──┤ loop:
ask(question) -> answer ◀──┤ messages = inbox()
inbox() -> [messages] ◀──┤
├────── shot = look(...)
│ Decide(goal, notes, history, screenshot) -> Decision
│ observation, verdict on the last step,
│ candidates, remember, plan, action, expectation
└────── for op in action.ops(shot): act(op)
-
One loop, in BAML. Python passes
lookandactintorun_taskas callables, both traced with@trace.instrument, so a run is one trace tree. -
Typed action space (
agent/actions.baml):click,double_click,type,key,scroll,drag,hover,open_app,open_url,wait,done,give_up. Each action compiles itself to screen-space ops; Python never does coordinate math. -
The decision schema does the reasoning. The model writes, in order: what it sees, whether its last action worked, the candidate moves with screen positions, facts to
remember, its plan, the action, and what it expects to see next. -
Trajectory as conversation, cached. Each past step is replayed as a user turn (its screenshot) and an assistant turn (the model's own decision). Earlier turns never change, and a
${cache()}marker goes before the current turn, so Claude reads the trajectory from Anthropic's prompt cache: each step writes only its new screenshot and decision to the cache. Claude replays up to 30 frames; local models replay 2, and older steps fall back to one-line summaries. -
Asking and steering. The model can
ask_userwhen the goal is ambiguous ("message Bob": which app?) or before an irreversible step. The run pauses until you answer, in the dashboard (one-click options or free text) or in the terminal. You can also message a running agent from the dashboard ("use Slack", "the button is top right"). It reads your message at the start of its next turn, treats it as overriding its plan, and keeps it in its notes. -
More steps on demand. When a run uses up its step budget, it pauses instead of ending: add 10, 20, 30 or any number of steps from the dashboard (or the terminal), or stop there. The model is told how many steps it gained. If nobody answers within 30 minutes, the run ends as out of steps.
-
Back to the dashboard. When a run finishes, gives up, fails, asks a question or runs out of steps, the server brings the dashboard tab back to the front, so you don't have to watch the screen. On macOS it switches to the tab in Chrome, Arc, Brave, Edge or Safari (macOS asks once to let your terminal control the browser). Other browsers are matched by window title. On Windows it raises the browser window showing the dashboard. If no open tab is found, it opens the run in a new one. Once you answer or add steps, the window the agent was working in comes back to the front before its next screenshot (on macOS, the exact browser tab). On Windows, if the dashboard shares a browser window with the agent's tab, the model switches tabs back itself.
-
Healing. A reply cut off at the output limit (e.g. the model started writing raw tool-call markup) is retried with a hint to answer briefly, instead of ending the run. An error in a background task is logged instead of killing the server.
-
Memory. Facts the model saves (text to copy, names, numbers) are fed back every turn. Without this, it loses information as soon as it switches apps and ping-pongs between them.
-
Loop detection. Warnings when the same action repeats, keeps coming back, keeps failing, or the step budget is nearly spent.
-
Coordinate spaces. Claude answers in screenshot pixels. Qwen3-VL answers on a 0–1000 grid.
Pilot.view()handles both. -
Cross-platform (
screen/platform.baml). Python reports the OS; BAML compiles each action for it. The prompt names the OS and its shortcut modifier (cmdon macOS,ctrlelsewhere). Key names map per OS, so a model'scmd+lbecomesctrl+lon Windows. Apps and URLs open the native way:open_appopen_urlmacOS open -a Appopen URLWindows Win, type the name, Enter Win+R, type the URL, Enter Linux Super, type the name, Enter xdg-open URL
Needs uv and the BAML CLI (brew install baml on macOS).
uv sync
baml generate # writes the typed Python SDK (baml_sdk/)Permissions:
- macOS: grant your terminal Accessibility and Screen Recording (System Settings → Privacy & Security).
- Windows: no setup, but input can't reach elevated (admin) windows unless the agent runs elevated too.
- Linux: an X11 session (pyautogui doesn't support Wayland), plus
scrotfor screenshots.
macOS is tested end to end. Windows and Linux are untested on real machines: their action compilation is unit-tested, and the Windows path was dry-run through the full loop.
Models
- LM Studio (default): download
qwen3-vl-4b-instructand start the local server (Settings → Local API server). The default URL ishttp://127.0.0.1:1235/v1; change it with--base-url. Give the model a context length of at least 8192. - Claude: set
ANTHROPIC_API_KEY(in your shell, or in a.envfile that the start scripts load). The UI only offers Claude when the key is set, as Claude Sonnet or Claude Opus. Opus is slower and costs more per step, but it's steadier on long, fiddly runs. The dashboard remembers the model and step budget you picked last.
Traces. The mode is picked at startup, and the server prints which one it chose:
- Boundary cloud: used when
BOUNDARY_API_KEYis a real key, orbaml auth loginis verified. Set[boundary] projectinbaml.tomlto yourorg/project. - Local: used when
BOUNDARY_API_KEY=local, or when there are no cloud credentials. Traces go to this project's.baml/, where the dashboard queries them.
uv run server.py # dashboard + API at http://127.0.0.1:8787
uv run server.py --provider claude # make Claude the default
uv run agent.py "Open Calculator and compute 12 * 34"
uv run agent.py --model qwen3-vl-8b-instruct "..."
uv run agent.py --provider claude "..."
uv run agent.py --provider claude --model claude-opus-5-5 "..."Or use the start scripts. They load .env (KEY=value lines, such as ANTHROPIC_API_KEY=...) and pass extra arguments on to server.py:
| Terminal | Double-click | |
|---|---|---|
| macOS / Linux | ./start.sh --provider claude |
start-server.command (macOS, opens Terminal) |
| Windows | .\start.ps1 --provider claude |
start-server.cmd (its own window, so the server survives your editor restarting) |
Slam the mouse into a screen corner to abort a run.
POST /api/execute |
{"goal", "max_steps"?, "provider"?, "model"?} queues a goal (one at a time: there's one mouse) |
GET /api/execute |
active run, queue, recent local runs |
POST /api/execute/<run_id>/cancel |
cancel |
POST /api/execute/<run_id>/answer |
{"answer"} answers the agent's question (ask_user) |
POST /api/execute/<run_id>/message |
{"text"} steers a running agent; it reads the message next turn |
POST /api/execute/<run_id>/extend |
{"steps"} gives a run that's out of steps 1–500 more; 0 ends it |
GET /api/query/runs |
run summaries from baml query |
GET /api/query/runs/<run_id> |
one run, step by step |
POST /api/query/sql |
{"sql": "SELECT ..."}, read-only |
GET /api/models |
LM Studio vision models, plus Claude if a key is set |
The frontend (web/) is a client-side React app with no build step.
agent.py Python host: look/act + CLI
server.py HTTP server: executor + query API + static frontend
web/ React dashboard
baml_src/
screen/platform.baml per-OS key names, shortcut modifier, app/URL launching
clients.baml model selection (Pilot): LM Studio / Claude, coordinate spaces
screen/ops.baml screen ops, coordinate mapping, key aliases
agent/actions.baml action space; each action compiles to ops
agent/decide_prompt.baml the decision prompt + schema
agent/agent.baml the loop: memory, history, loop detection, outcomes
api/query.baml typed `baml query` client (cloud or local)
api/runs.baml run list / run detail for the dashboard
api/models.baml available models
Tests run offline, with no model calls: baml test.