Publish tiny_computer_agent_app.py from marimo
Browse files- tiny_computer_agent_app.py +93 -65
tiny_computer_agent_app.py
CHANGED
|
@@ -44,10 +44,10 @@ def readme():
|
|
| 44 |
No cloud API, no API keys, no telemetry β and a core that imports nothing outside the
|
| 45 |
Python standard library.
|
| 46 |
|
| 47 |
-
> **Status 0.
|
| 48 |
-
>
|
| 49 |
-
> harness
|
| 50 |
-
>
|
| 51 |
|
| 52 |
## The idea, in one loop
|
| 53 |
|
|
@@ -68,59 +68,65 @@ def readme():
|
|
| 68 |
2. **Think** β the model is shown a *short numbered menu* built from that index and
|
| 69 |
answers with one number (plus a verb). It never emits coordinates.
|
| 70 |
3. **Act** β the harness maps the chosen label back to pixels and executes it.
|
| 71 |
-
4. **Verify** β re-read the screen
|
| 72 |
-
|
| 73 |
|
| 74 |
## Measured results
|
| 75 |
|
| 76 |
-
Everything below was run from this repo
|
| 77 |
-
|
| 78 |
-
is aspirational.
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
```
|
| 84 |
-
$ python -m tiny_computer_agent --
|
|
|
|
| 85 |
index: auto - OCR when it finds text, grid otherwise (rapidocr-onnxruntime ready (CPU, offline))
|
| 86 |
-
elements detected: 5
|
| 87 |
|
| 88 |
-
step 0 | click label='Save' reason="matched 'Save' in the element index" | ok:
|
| 89 |
-
|
|
|
|
| 90 |
```
|
| 91 |
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
```
|
| 95 |
-
$ python -m tiny_computer_agent --image example --brain
|
| 96 |
elements detected: 5
|
| 97 |
-
|
| 98 |
-
step 0 | click label='Save' reason="the reply names 'Save'" | ok: (dry-run) would click -> pixel (295, 284)
|
| 99 |
-
step 1 | click label='Save' ...
|
| 100 |
-
step 2 | ... ERROR no progress: repeating click label='Save' against an unchanged screen
|
| 101 |
```
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
loop that a real desktop would have moved past.
|
| 106 |
-
|
| 107 |
-
**A 0.5B text planner over the OCR index β Qwen2.5-0.5B-Instruct β is not reliable:**
|
| 108 |
|
| 109 |
```
|
| 110 |
-
|
| 111 |
-
step 0 | click label='Cancel' reason='menu index 5'
|
| 112 |
-
step 1 | click label='Files' reason="recovered 'Files' from the model's reply: {\"kind\": \"click\", \"index\": 0, ...}"
|
| 113 |
-
step 2 | click label='Save' reason='menu index 4'
|
| 114 |
-
result: failure - step budget of 3 exhausted
|
| 115 |
```
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
**
|
| 123 |
-
|
| 124 |
|
| 125 |
## Install
|
| 126 |
|
|
@@ -128,7 +134,7 @@ def readme():
|
|
| 128 |
pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
|
| 129 |
pip install -e ".[desktop,ocr]" # + real capture and real input
|
| 130 |
pip install -e ".[vision,ocr]" # + the tiny model brains
|
| 131 |
-
pytest #
|
| 132 |
```
|
| 133 |
|
| 134 |
## Run it on your machine
|
|
@@ -137,18 +143,37 @@ def readme():
|
|
| 137 |
python -m tiny_computer_agent --doctor # what can this machine do?
|
| 138 |
python -m tiny_computer_agent --image example # replay a screenshot, dry run
|
| 139 |
python -m tiny_computer_agent --live # your real screen, dry run
|
| 140 |
-
python -m tiny_computer_agent --live --real-input "click 'OK'"
|
|
|
|
| 141 |
```
|
| 142 |
|
| 143 |
`--doctor` is the first thing to run on a new machine: it checks the display session,
|
| 144 |
-
capture backend, pyautogui, the OCR engine, the model runtime
|
| 145 |
-
prints the
|
| 146 |
|
| 147 |
```bash
|
| 148 |
export TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1
|
| 149 |
-
python -m tiny_computer_agent --live --real-input "click 'Save'"
|
| 150 |
```
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
## How it works
|
| 153 |
|
| 154 |
| Module | Job |
|
|
@@ -156,57 +181,60 @@ def readme():
|
|
| 156 |
| `actions.py` | The closed vocabulary β `click` `type` `key` `scroll` `wait` `done`, validated before anything executes |
|
| 157 |
| `observe.py` | `Element`/`Observation`, `FakeScreen`, `ImageScreen`, `GrabScreen` (mss β Pillow fallback) |
|
| 158 |
| `perception.py` | Detectors: `OcrDetector` (RapidOCR), `GridDetector`, `AutoDetector`, `IndexedScreen` |
|
| 159 |
-
| `brain.py` | `DecisionMenu` (the numbered menu), `ModelBrain`, lenient `parse_decision`, `MockBrain`, `PlannerBrain` |
|
| 160 |
| `models.py` | `VLMBrain` (SmolVLM-256M) and `TextPlannerBrain` (Qwen2.5-0.5B), lazily loaded |
|
| 161 |
| `act.py` | `DryRunExecutor` (the default) and `PyAutoGuiExecutor` |
|
| 162 |
| `safety.py` | `Policy`: deny lists, kill switch, step budget, the blind-grid-click guard |
|
| 163 |
-
| `loop.py` | `AgentLoop`, `Episode`, `Step`,
|
| 164 |
| `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
|
| 165 |
| `demo.py` | The synthetic desktop used by tests and `--image example` |
|
| 166 |
|
| 167 |
## Why a tiny model can do this at all
|
| 168 |
|
| 169 |
-
The harness does the heavy lifting, so the model never has to be clever:
|
| 170 |
-
|
| 171 |
| Job | Who does it |
|
| 172 |
|---|---|
|
| 173 |
| Finding *where* a button is | OCR / accessibility tree β **not** the model |
|
| 174 |
| Choosing *what* to do | the model, but only by picking a number from a short menu |
|
| 175 |
-
| Knowing whether it worked | the
|
| 176 |
| Recovering from a bad step | the retry policy in `loop.py`, not the weights |
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
|
|
|
| 180 |
|
| 181 |
## Safety, because this thing has your mouse
|
| 182 |
|
| 183 |
- Run it in a **VM, container, or throwaway desktop account**, not your real session.
|
| 184 |
- **Dry run is the default**, so you can read a whole episode before anything clicks.
|
| 185 |
-
- Deny lists for typing (`sudo`, `rm -rf`, "password"),
|
| 186 |
-
wallet, bank) and
|
| 187 |
- A **kill-switch file** checked before every action, plus a hard step budget.
|
| 188 |
-
- **Blind grid clicks are refused with real input** unless you pass `--allow-grid`
|
| 189 |
-
OCR finds nothing
|
| 190 |
-
|
|
|
|
|
|
|
| 191 |
- **Local-first, for real:** pixels and keystrokes never leave the machine.
|
| 192 |
|
| 193 |
## What works, and what does not
|
| 194 |
|
| 195 |
-
**Works:** real capture, a real OCR index with
|
| 196 |
-
|
| 197 |
-
the doctor, the CLI,
|
|
|
|
| 198 |
|
| 199 |
-
**Does not work yet:**
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
|
| 204 |
## Roadmap
|
| 205 |
|
| 206 |
- [x] Capture, OCR index, closed vocabulary, dry-run default, policy gates, doctor
|
| 207 |
-
- [x] A
|
| 208 |
-
- [
|
| 209 |
-
- [ ]
|
|
|
|
| 210 |
- [ ] Score episodes: replay a trace and count how often the choice was right
|
| 211 |
- [ ] marimo console: live screen preview, action log, pause / resume / step
|
| 212 |
|
|
@@ -229,7 +257,7 @@ def repo_files():
|
|
| 229 |
|
| 230 |
[project]
|
| 231 |
name = "tiny-computer-agent"
|
| 232 |
-
version = "0.
|
| 233 |
description = "An agentic harness that drives a real computer with a tiny, fully local model - no cloud API."
|
| 234 |
readme = "README.md"
|
| 235 |
requires-python = ">=3.10"
|
|
|
|
| 44 |
No cloud API, no API keys, no telemetry β and a core that imports nothing outside the
|
| 45 |
Python standard library.
|
| 46 |
|
| 47 |
+
> **Status 0.3.0: it drives a real desktop.** Screen capture, an OCR element index with
|
| 48 |
+
> real coordinates, a real mouse click, and a *verified* outcome β all through the
|
| 49 |
+
> harness, in one episode. What is not solved is model reliability: see
|
| 50 |
+
> [measured results](#measured-results).
|
| 51 |
|
| 52 |
## The idea, in one loop
|
| 53 |
|
|
|
|
| 68 |
2. **Think** β the model is shown a *short numbered menu* built from that index and
|
| 69 |
answers with one number (plus a verb). It never emits coordinates.
|
| 70 |
3. **Act** β the harness maps the chosen label back to pixels and executes it.
|
| 71 |
+
4. **Verify** β re-read the screen; stop if the same action repeats against an unchanged
|
| 72 |
+
screen, and only call the episode a success if a verifier can see it worked.
|
| 73 |
|
| 74 |
## Measured results
|
| 75 |
|
| 76 |
+
Everything below was run from this repo on CPU, dry-run unless stated. Nothing is
|
| 77 |
+
aspirational.
|
|
|
|
| 78 |
|
| 79 |
+
### On a live desktop, with a real click
|
| 80 |
+
|
| 81 |
+
A real Tk window (a Save dialog with Save/Cancel buttons) on a **real X server**
|
| 82 |
+
(`Xvfb :99`), driven with real mouse input. The button sits at window `+120+120`,
|
| 83 |
+
`x=20, y=155`, `110x40`, so its centre is `(195, 295)`:
|
| 84 |
|
| 85 |
```
|
| 86 |
+
$ python -m tiny_computer_agent --live --real-input --verify-text Saved "click 'Save' to save the document"
|
| 87 |
+
mode: REAL INPUT (mouse and keyboard live) | screen: real desktop (monitor 1) | brain: planner
|
| 88 |
index: auto - OCR when it finds text, grid otherwise (rapidocr-onnxruntime ready (CPU, offline))
|
|
|
|
| 89 |
|
| 90 |
+
step 0 | click label='Save' reason="matched 'Save' in the element index" | ok: clicked (195, 296)
|
| 91 |
+
|
| 92 |
+
result: success - verified after 1 action(s)
|
| 93 |
```
|
| 94 |
|
| 95 |
+
The click landed on the button, the app changed its own window to "Saved", and the
|
| 96 |
+
verifier confirmed it before the episode was called a success. **Pixels in, a real click
|
| 97 |
+
out, and evidence that it worked.**
|
| 98 |
+
|
| 99 |
+
### On a replayed screenshot
|
| 100 |
|
| 101 |
```
|
| 102 |
+
$ python -m tiny_computer_agent --image example --brain planner
|
| 103 |
elements detected: 5
|
| 104 |
+
step 0 | click label='Save' ... -> pixel (295, 284) # OCR found the button
|
|
|
|
|
|
|
|
|
|
| 105 |
```
|
| 106 |
|
| 107 |
+
**SmolVLM-256M-Instruct** (256M parameters, CPU) read the button off the pixels and
|
| 108 |
+
named the right target:
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
```
|
| 111 |
+
step 0 | click label='Save' reason="the reply names 'Save'" -> pixel (295, 284)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
```
|
| 113 |
|
| 114 |
+
### What did *not* work, and what caught it
|
| 115 |
+
|
| 116 |
+
- **The 256M VLM echoes the prompt on a busy live screen.** Asked to click Save on the
|
| 117 |
+
live Tk window it replied with the prompt text back. The harness's echo detection
|
| 118 |
+
refused it (`BrainError: the reply mentions 4 menu entries, so it looks like an echo of
|
| 119 |
+
the prompt`) and stopped β **no blind click**. On the cleaner synthetic screenshot the
|
| 120 |
+
same model chose correctly, so this is a prompt/robustness problem, not a wiring one.
|
| 121 |
+
- **Qwen2.5-0.5B text planner over the OCR index is not reliable**: it clicked Cancel
|
| 122 |
+
before Save, then recovered a garbled JSON reply, then found Save β budget exhausted.
|
| 123 |
+
- **The grid fallback announced itself.** After the dialog closed, OCR found no text, the
|
| 124 |
+
index degraded to 96 numbered cells, and the run printed
|
| 125 |
+
`warning: no text was found on screen, so the index is 96 numbered grid cells and the
|
| 126 |
+
agent is choosing blind` β instead of silently guessing.
|
| 127 |
|
| 128 |
+
**123 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
|
| 129 |
+
that click a real button.
|
| 130 |
|
| 131 |
## Install
|
| 132 |
|
|
|
|
| 134 |
pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
|
| 135 |
pip install -e ".[desktop,ocr]" # + real capture and real input
|
| 136 |
pip install -e ".[vision,ocr]" # + the tiny model brains
|
| 137 |
+
pytest # 123 tests, no display or network needed
|
| 138 |
```
|
| 139 |
|
| 140 |
## Run it on your machine
|
|
|
|
| 143 |
python -m tiny_computer_agent --doctor # what can this machine do?
|
| 144 |
python -m tiny_computer_agent --image example # replay a screenshot, dry run
|
| 145 |
python -m tiny_computer_agent --live # your real screen, dry run
|
| 146 |
+
python -m tiny_computer_agent --live --real-input "click 'OK'"
|
| 147 |
+
python -m tiny_computer_agent --live --real-input --verify-text "Saved" "click 'Save'"
|
| 148 |
```
|
| 149 |
|
| 150 |
`--doctor` is the first thing to run on a new machine: it checks the display session,
|
| 151 |
+
capture backend, pyautogui, the OCR engine, the model runtime and the kill switch, and
|
| 152 |
+
prints the fix for anything missing. Real input needs **two** deliberate acts:
|
| 153 |
|
| 154 |
```bash
|
| 155 |
export TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1
|
|
|
|
| 156 |
```
|
| 157 |
|
| 158 |
+
**Pass `--verify-text`.** Without it, a successful action that removes its own target
|
| 159 |
+
(clicking Save closes the dialog) is indistinguishable from a failure, and the episode is
|
| 160 |
+
recorded as one. With it, the harness only reports success when it can see the text it
|
| 161 |
+
was given.
|
| 162 |
+
|
| 163 |
+
### No desktop? Use a virtual one
|
| 164 |
+
|
| 165 |
+
Xvfb is enough for everything except a real model's hallucinations:
|
| 166 |
+
|
| 167 |
+
```bash
|
| 168 |
+
Xvfb :99 -screen 0 1280x800x24 &
|
| 169 |
+
export DISPLAY=:99
|
| 170 |
+
TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
That test starts its own Tk window, asserts OCR finds the button at the coordinates it
|
| 174 |
+
was actually drawn at, clicks it for real, and checks the app reacted. It is opt-in
|
| 175 |
+
because it moves your mouse β run it on a throwaway desktop.
|
| 176 |
+
|
| 177 |
## How it works
|
| 178 |
|
| 179 |
| Module | Job |
|
|
|
|
| 181 |
| `actions.py` | The closed vocabulary β `click` `type` `key` `scroll` `wait` `done`, validated before anything executes |
|
| 182 |
| `observe.py` | `Element`/`Observation`, `FakeScreen`, `ImageScreen`, `GrabScreen` (mss β Pillow fallback) |
|
| 183 |
| `perception.py` | Detectors: `OcrDetector` (RapidOCR), `GridDetector`, `AutoDetector`, `IndexedScreen` |
|
| 184 |
+
| `brain.py` | `DecisionMenu` (the numbered menu), `ModelBrain`, lenient `parse_decision`, echo detection, `MockBrain`, `PlannerBrain` |
|
| 185 |
| `models.py` | `VLMBrain` (SmolVLM-256M) and `TextPlannerBrain` (Qwen2.5-0.5B), lazily loaded |
|
| 186 |
| `act.py` | `DryRunExecutor` (the default) and `PyAutoGuiExecutor` |
|
| 187 |
| `safety.py` | `Policy`: deny lists, kill switch, step budget, the blind-grid-click guard |
|
| 188 |
+
| `loop.py` | `AgentLoop`, `Episode`, `Step`, the no-progress guard and the verifier |
|
| 189 |
| `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
|
| 190 |
| `demo.py` | The synthetic desktop used by tests and `--image example` |
|
| 191 |
|
| 192 |
## Why a tiny model can do this at all
|
| 193 |
|
|
|
|
|
|
|
| 194 |
| Job | Who does it |
|
| 195 |
|---|---|
|
| 196 |
| Finding *where* a button is | OCR / accessibility tree β **not** the model |
|
| 197 |
| Choosing *what* to do | the model, but only by picking a number from a short menu |
|
| 198 |
+
| Knowing whether it worked | the verifier and the no-progress guard, not the model's word |
|
| 199 |
| Recovering from a bad step | the retry policy in `loop.py`, not the weights |
|
| 200 |
|
| 201 |
+
A 256M model cannot emit `(295, 284)` reliably, but it can pick "4" out of five options β
|
| 202 |
+
and the harness owns the coordinate lookup, the vocabulary check and the definition of
|
| 203 |
+
success.
|
| 204 |
|
| 205 |
## Safety, because this thing has your mouse
|
| 206 |
|
| 207 |
- Run it in a **VM, container, or throwaway desktop account**, not your real session.
|
| 208 |
- **Dry run is the default**, so you can read a whole episode before anything clicks.
|
| 209 |
+
- Deny lists for typing (`sudo`, `rm -rf`, "password"), widgets (terminal, keychain,
|
| 210 |
+
wallet, bank) and key chords.
|
| 211 |
- A **kill-switch file** checked before every action, plus a hard step budget.
|
| 212 |
+
- **Blind grid clicks are refused with real input** unless you pass `--allow-grid`: when
|
| 213 |
+
OCR finds nothing, the fallback index is coordinates with no meaning, and a model
|
| 214 |
+
choosing one is guessing about your desktop.
|
| 215 |
+
- **A brain's "done" is a claim, not evidence**: pass `--verify-text` and the harness
|
| 216 |
+
decides success itself.
|
| 217 |
- **Local-first, for real:** pixels and keystrokes never leave the machine.
|
| 218 |
|
| 219 |
## What works, and what does not
|
| 220 |
|
| 221 |
+
**Works:** real capture, a real OCR index with coordinates that land on the right pixel,
|
| 222 |
+
a real click that an app reacts to, verified success, the closed vocabulary, the policy
|
| 223 |
+
gates, the no-progress guard, echo detection, the doctor, the CLI, 123 tests β and a
|
| 224 |
+
256M VLM that reads a button label from pixels on a clean screenshot.
|
| 225 |
|
| 226 |
+
**Does not work yet:** reliability on a *live* screen. The 256M VLM echoed the prompt
|
| 227 |
+
there; the 0.5B text planner picks wrong actions; the accessibility-tree detector is not
|
| 228 |
+
implemented, so labels come from OCR only; and the harness still trusts a model's `done`
|
| 229 |
+
when no verifier is given.
|
| 230 |
|
| 231 |
## Roadmap
|
| 232 |
|
| 233 |
- [x] Capture, OCR index, closed vocabulary, dry-run default, policy gates, doctor
|
| 234 |
+
- [x] A real click on a real window, with a verified outcome
|
| 235 |
+
- [x] Blind-grid-click guard, echo detection, no-progress guard
|
| 236 |
+
- [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
|
| 237 |
+
- [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
|
| 238 |
- [ ] Score episodes: replay a trace and count how often the choice was right
|
| 239 |
- [ ] marimo console: live screen preview, action log, pause / resume / step
|
| 240 |
|
|
|
|
| 257 |
|
| 258 |
[project]
|
| 259 |
name = "tiny-computer-agent"
|
| 260 |
+
version = "0.3.0"
|
| 261 |
description = "An agentic harness that drives a real computer with a tiny, fully local model - no cloud API."
|
| 262 |
readme = "README.md"
|
| 263 |
requires-python = ">=3.10"
|