Publish README.md from marimo
Browse files
README.md
CHANGED
|
@@ -18,12 +18,13 @@ tags:
|
|
| 18 |
# π±οΈ Tiny Computer Agent
|
| 19 |
|
| 20 |
**An agentic harness that drives a real computer with a tiny, fully local model.**
|
| 21 |
-
No cloud API, no API keys, no telemetry β and a core that imports nothing outside
|
| 22 |
-
|
| 23 |
|
| 24 |
-
> **Status:
|
| 25 |
-
>
|
| 26 |
-
>
|
|
|
|
| 27 |
|
| 28 |
## The idea, in one loop
|
| 29 |
|
|
@@ -38,143 +39,153 @@ the Python standard library.
|
|
| 38 |
repeat until done, or the step budget runs out
|
| 39 |
```
|
| 40 |
|
| 41 |
-
1. **Observe** β
|
| 42 |
-
accessibility tree
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
| 48 |
|
| 49 |
-
##
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
```
|
| 52 |
-
$ python -m tiny_computer_agent
|
| 53 |
-
goal: click 'Save' to save the document
|
| 54 |
-
mode: dry-run | screen: scripted demo | kill switch: create /tmp/tiny-computer-agent.STOP to stop
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
```
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
- **three brains** β `MockBrain` replays a script; `PlannerBrain` is rule-based and
|
| 73 |
-
genuinely clicks the element matching the goal (it waits for a dialog instead of
|
| 74 |
-
flailing, and refuses to click the same label twice); `VLMBrain` is the seam β
|
| 75 |
-
prompt assembly and reply validation are done, `_generate()` is yours.
|
| 76 |
-
- **two executors** β `DryRunExecutor` is the default: it logs the plan and resolves
|
| 77 |
-
every label to a pixel. `PyAutoGuiExecutor` does real input and **refuses to click
|
| 78 |
-
when a label matches no element**, because a tiny model guessing pixels is how you
|
| 79 |
-
click "Delete account". Dry-run fails exactly where live input refuses, so a
|
| 80 |
-
rehearsal never gives false confidence.
|
| 81 |
-
- **policy gates** β deny-listed text (`sudo`, `rm -rf`, "password", "api_key"),
|
| 82 |
-
deny-listed widgets (terminal, keychain, password manager, wallet, bank), four
|
| 83 |
-
deny-listed key chords, a kill-switch file checked before every action, and a step
|
| 84 |
-
budget.
|
| 85 |
-
- **real input needs two deliberate flags** β `--real-input` *and*
|
| 86 |
-
`TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1`. Default is a dry run.
|
| 87 |
-
- **56 tests** needing no display, no model and no network.
|
| 88 |
-
|
| 89 |
-
What does **not** work yet: seeing a real screen *usefully*. `GrabScreen` captures
|
| 90 |
-
pixels through `mss`, but nothing yet turns pixels into an element index (OCR or
|
| 91 |
-
AT-SPI), so a live screen currently yields zero elements β which is exactly what the
|
| 92 |
-
label path depends on. And no model is wired up.
|
| 93 |
-
|
| 94 |
-
## Install and run
|
| 95 |
|
| 96 |
```bash
|
| 97 |
-
pip install -e .
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
```
|
| 102 |
|
| 103 |
-
##
|
| 104 |
|
| 105 |
-
```
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
```
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
## Why
|
| 121 |
|
| 122 |
-
The harness does the heavy lifting, so the model
|
| 123 |
|
| 124 |
| Job | Who does it |
|
| 125 |
|---|---|
|
| 126 |
-
| Finding *where* a button is | OCR / accessibility tree
|
| 127 |
-
| Choosing *what* to do |
|
| 128 |
-
| Knowing whether it worked | the next frame, plus
|
| 129 |
| Recovering from a bad step | the retry policy in `loop.py`, not the weights |
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
|
| 134 |
-
##
|
| 135 |
-
|
| 136 |
-
Pluggable behind one interface; nothing here needs a network call at run time.
|
| 137 |
-
|
| 138 |
-
- **Moondream 2** / **SmolVLM-256M / 500M** β a few hundred MB, safe default
|
| 139 |
-
- **Qwen2-VL-2B-Instruct**, **Florence-2-base** β stronger grounding, still tiny
|
| 140 |
-
- **OCR + text-only planner** β no VLM at all; the cheapest credible baseline
|
| 141 |
-
- **mock brain** β deterministic scripted actions, used to test the harness itself
|
| 142 |
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
-
|
| 146 |
-
tiny-computer-agent/
|
| 147 |
-
βββ README.md
|
| 148 |
-
βββ LICENSE
|
| 149 |
-
βββ pyproject.toml
|
| 150 |
-
βββ tiny_computer_agent_app.py # marimo console: run an episode, publish this card
|
| 151 |
-
βββ tiny_computer_agent/
|
| 152 |
-
β βββ actions.py # the closed action vocabulary
|
| 153 |
-
β βββ observe.py # Observation + element index; FakeScreen, GrabScreen
|
| 154 |
-
β βββ brain.py # MockBrain, PlannerBrain, VLMBrain (the seam)
|
| 155 |
-
β βββ act.py # DryRunExecutor (default) + PyAutoGuiExecutor
|
| 156 |
-
β βββ safety.py # Policy: deny lists, kill switch, step budget
|
| 157 |
-
β βββ loop.py # AgentLoop, Episode, Step
|
| 158 |
-
β βββ __main__.py # CLI
|
| 159 |
-
βββ tests/ # 56 tests: no display, no model, no network
|
| 160 |
-
```
|
| 161 |
|
| 162 |
-
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
-
|
| 167 |
-
|
| 168 |
-
- **Local-first, for real:** screen pixels and keystrokes never leave the machine.
|
| 169 |
|
| 170 |
## Roadmap
|
| 171 |
|
| 172 |
-
- [x]
|
| 173 |
-
- [x]
|
| 174 |
-
- [ ]
|
| 175 |
-
- [ ]
|
|
|
|
| 176 |
- [ ] marimo console: live screen preview, action log, pause / resume / step
|
| 177 |
-
- [ ] persist episodes, so failed runs can be replayed and scored
|
| 178 |
|
| 179 |
## License
|
| 180 |
|
|
|
|
| 18 |
# π±οΈ Tiny Computer Agent
|
| 19 |
|
| 20 |
**An agentic harness that drives a real computer with a tiny, fully local model.**
|
| 21 |
+
No cloud API, no API keys, no telemetry β and a core that imports nothing outside the
|
| 22 |
+
Python standard library.
|
| 23 |
|
| 24 |
+
> **Status 0.2.0: it runs, on pixels.** The screen is captured for real, OCR turns it
|
| 25 |
+
> into a short labelled menu, a **256M** vision model picks from that menu, and the
|
| 26 |
+
> harness resolves the choice to a real pixel and (behind two locks) clicks it.
|
| 27 |
+
> What is *not* solved is model accuracy β see [measured results](#measured-results).
|
| 28 |
|
| 29 |
## The idea, in one loop
|
| 30 |
|
|
|
|
| 39 |
repeat until done, or the step budget runs out
|
| 40 |
```
|
| 41 |
|
| 42 |
+
1. **Observe** β capture the screen, then build an **element index**: OCR (and,
|
| 43 |
+
optionally, the OS accessibility tree) locates text and widgets and gives each one a
|
| 44 |
+
label and a real screen coordinate.
|
| 45 |
+
2. **Think** β the model is shown a *short numbered menu* built from that index and
|
| 46 |
+
answers with one number (plus a verb). It never emits coordinates.
|
| 47 |
+
3. **Act** β the harness maps the chosen label back to pixels and executes it.
|
| 48 |
+
4. **Verify** β re-read the screen, and stop if the same action repeats against an
|
| 49 |
+
unchanged screen, because that is a stuck agent, not a persistent one.
|
| 50 |
|
| 51 |
+
## Measured results
|
| 52 |
|
| 53 |
+
Everything below was run from this repo, **CPU only, dry-run**, against the synthetic
|
| 54 |
+
screenshot (`--image example`), because the build machine has no desktop. Nothing here
|
| 55 |
+
is aspirational.
|
| 56 |
+
|
| 57 |
+
**A rule-based planner, end to end:** OCR read five labels off the screenshot, matched
|
| 58 |
+
the goal, and resolved it to a pixel.
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
$ python -m tiny_computer_agent --image example --brain planner
|
| 62 |
+
index: auto - OCR when it finds text, grid otherwise (rapidocr-onnxruntime ready (CPU, offline))
|
| 63 |
+
elements detected: 5
|
| 64 |
+
|
| 65 |
+
step 0 | click label='Save' reason="matched 'Save' in the element index" | ok: (dry-run) would click -> pixel (295, 284)
|
| 66 |
+
step 1 | done reason="already clicked 'Save'; goal assumed met" | ok: goal reported met
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
**A 256M vision model β SmolVLM-256M-Instruct, CPU β read the button off the pixels:**
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
$ python -m tiny_computer_agent --image example --brain vlm
|
| 73 |
+
elements detected: 5
|
| 74 |
+
|
| 75 |
+
step 0 | click label='Save' reason="the reply names 'Save'" | ok: (dry-run) would click -> pixel (295, 284)
|
| 76 |
+
step 1 | click label='Save' ...
|
| 77 |
+
step 2 | ... ERROR no progress: repeating click label='Save' against an unchanged screen
|
| 78 |
```
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
It named the correct target from pixels alone. The episode is recorded as a *failure*
|
| 81 |
+
only because the replayed screenshot never changes, so the no-progress guard stops a
|
| 82 |
+
loop that a real desktop would have moved past.
|
| 83 |
|
| 84 |
+
**A 0.5B text planner over the OCR index β Qwen2.5-0.5B-Instruct β is not reliable:**
|
| 85 |
+
|
| 86 |
+
```
|
| 87 |
+
$ python -m tiny_computer_agent --image example --brain text-planner
|
| 88 |
+
step 0 | click label='Cancel' reason='menu index 5'
|
| 89 |
+
step 1 | click label='Files' reason="recovered 'Files' from the model's reply: {"kind": "click", "index": 0, ...}"
|
| 90 |
+
step 2 | click label='Save' reason='menu index 4'
|
| 91 |
+
result: failure - step budget of 3 exhausted
|
| 92 |
```
|
| 93 |
|
| 94 |
+
It clicked Cancel before Save, then wandered. The harness held up regardless: every
|
| 95 |
+
action was validated against the closed vocabulary, a garbled JSON reply was recovered
|
| 96 |
+
into a usable action, and the budget stopped it. **A tiny VLM beats a tiny text model
|
| 97 |
+
here** β which is why the vision path is the recommended one.
|
| 98 |
+
|
| 99 |
+
**116 tests pass**, including real OCR over a real image asserting the `Save` button's
|
| 100 |
+
coordinates, and a screenshot replay that runs the whole pipeline with no display.
|
| 101 |
+
|
| 102 |
+
## Install
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
```bash
|
| 105 |
+
pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
|
| 106 |
+
pip install -e ".[desktop,ocr]" # + real capture and real input
|
| 107 |
+
pip install -e ".[vision,ocr]" # + the tiny model brains
|
| 108 |
+
pytest # 116 tests, no display or network needed
|
| 109 |
```
|
| 110 |
|
| 111 |
+
## Run it on your machine
|
| 112 |
|
| 113 |
+
```bash
|
| 114 |
+
python -m tiny_computer_agent --doctor # what can this machine do?
|
| 115 |
+
python -m tiny_computer_agent --image example # replay a screenshot, dry run
|
| 116 |
+
python -m tiny_computer_agent --live # your real screen, dry run
|
| 117 |
+
python -m tiny_computer_agent --live --real-input "click 'OK'" # needs the env var too
|
| 118 |
+
```
|
| 119 |
|
| 120 |
+
`--doctor` is the first thing to run on a new machine: it checks the display session,
|
| 121 |
+
capture backend, pyautogui, the OCR engine, the model runtime, and the kill switch, and
|
| 122 |
+
prints the exact fix for anything missing. Real input needs **two** deliberate acts:
|
| 123 |
|
| 124 |
+
```bash
|
| 125 |
+
export TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1
|
| 126 |
+
python -m tiny_computer_agent --live --real-input "click 'Save'"
|
| 127 |
```
|
| 128 |
|
| 129 |
+
## How it works
|
| 130 |
+
|
| 131 |
+
| Module | Job |
|
| 132 |
+
|---|---|
|
| 133 |
+
| `actions.py` | The closed vocabulary β `click` `type` `key` `scroll` `wait` `done`, validated before anything executes |
|
| 134 |
+
| `observe.py` | `Element`/`Observation`, `FakeScreen`, `ImageScreen`, `GrabScreen` (mss β Pillow fallback) |
|
| 135 |
+
| `perception.py` | Detectors: `OcrDetector` (RapidOCR), `GridDetector`, `AutoDetector`, `IndexedScreen` |
|
| 136 |
+
| `brain.py` | `DecisionMenu` (the numbered menu), `ModelBrain`, lenient `parse_decision`, `MockBrain`, `PlannerBrain` |
|
| 137 |
+
| `models.py` | `VLMBrain` (SmolVLM-256M) and `TextPlannerBrain` (Qwen2.5-0.5B), lazily loaded |
|
| 138 |
+
| `act.py` | `DryRunExecutor` (the default) and `PyAutoGuiExecutor` |
|
| 139 |
+
| `safety.py` | `Policy`: deny lists, kill switch, step budget, the blind-grid-click guard |
|
| 140 |
+
| `loop.py` | `AgentLoop`, `Episode`, `Step`, and the no-progress guard |
|
| 141 |
+
| `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
|
| 142 |
+
| `demo.py` | The synthetic desktop used by tests and `--image example` |
|
| 143 |
|
| 144 |
+
## Why a tiny model can do this at all
|
| 145 |
|
| 146 |
+
The harness does the heavy lifting, so the model never has to be clever:
|
| 147 |
|
| 148 |
| Job | Who does it |
|
| 149 |
|---|---|
|
| 150 |
+
| Finding *where* a button is | OCR / accessibility tree β **not** the model |
|
| 151 |
+
| Choosing *what* to do | the model, but only by picking a number from a short menu |
|
| 152 |
+
| Knowing whether it worked | the next frame, plus the no-progress guard |
|
| 153 |
| Recovering from a bad step | the retry policy in `loop.py`, not the weights |
|
| 154 |
|
| 155 |
+
The menu is the trick: a 256M model cannot emit `(295, 284)` reliably, but it can pick
|
| 156 |
+
"4" out of five options β and the harness owns the coordinate lookup.
|
| 157 |
|
| 158 |
+
## Safety, because this thing has your mouse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
- Run it in a **VM, container, or throwaway desktop account**, not your real session.
|
| 161 |
+
- **Dry run is the default**, so you can read a whole episode before anything clicks.
|
| 162 |
+
- Deny lists for typing (`sudo`, `rm -rf`, "password"), for widgets (terminal, keychain,
|
| 163 |
+
wallet, bank) and for key chords.
|
| 164 |
+
- A **kill-switch file** checked before every action, plus a hard step budget.
|
| 165 |
+
- **Blind grid clicks are refused with real input** unless you pass `--allow-grid`. When
|
| 166 |
+
OCR finds nothing on screen, the fallback index is a numbered grid β coordinates with
|
| 167 |
+
no meaning β and a model choosing one is guessing about your desktop.
|
| 168 |
+
- **Local-first, for real:** pixels and keystrokes never leave the machine.
|
| 169 |
|
| 170 |
+
## What works, and what does not
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
**Works:** real capture, a real OCR index with real coordinates, a 256M VLM choosing the
|
| 173 |
+
right labelled target, the closed vocabulary, the policy gates, the no-progress guard,
|
| 174 |
+
the doctor, the CLI, 116 tests.
|
| 175 |
|
| 176 |
+
**Does not work yet:** accuracy. A 0.5B text planner picks wrong actions; a 256M VLM can
|
| 177 |
+
answer "done" before doing anything, and the harness currently trusts that; the
|
| 178 |
+
accessibility-tree detector (AT-SPI / UI Automation) is not implemented; and nothing here
|
| 179 |
+
has been run against a live multi-window desktop, only against a synthetic screenshot.
|
|
|
|
| 180 |
|
| 181 |
## Roadmap
|
| 182 |
|
| 183 |
+
- [x] Capture, OCR index, closed vocabulary, dry-run default, policy gates, doctor
|
| 184 |
+
- [x] A 256M VLM that picks a labelled target from pixels
|
| 185 |
+
- [ ] Make the harness distrust a `done` that arrives before any action has been taken
|
| 186 |
+
- [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows) for exact widgets
|
| 187 |
+
- [ ] Score episodes: replay a trace and count how often the choice was right
|
| 188 |
- [ ] marimo console: live screen preview, action log, pause / resume / step
|
|
|
|
| 189 |
|
| 190 |
## License
|
| 191 |
|