hkblockchaintechnology commited on
Commit
8dfbc6a
·
verified ·
1 Parent(s): 2bef145

Publish README.md from marimo

Browse files
Files changed (1) hide show
  1. README.md +611 -8
README.md CHANGED
@@ -24,7 +24,7 @@ Python standard library.
24
 
25
  > **Status 0.3.0: it drives a real desktop — on Linux and on Windows.** Screen capture,
26
  > an OCR element index with real coordinates, a real mouse click, and a *verified*
27
- > outcome. A run can be saved as a portable *trace*, replayed offline and **scored**, so
28
  > "which brain is right, and how often?" is a number in a table.
29
  > The suite passes on a live Linux desktop and inside a real Windows Server
30
  > 2022 guest; what is not solved is model reliability — see
@@ -86,7 +86,7 @@ X server. First the suite, headless:
86
  ```bash
87
  python -m pip install -e ".[desktop,ocr,dev]"
88
  pytest tests -q
89
- # 151 passed, 2 skipped in 18.7s
90
  ```
91
 
92
  The two skips are the live-desktop tests. Give them a display and they run for real:
@@ -218,7 +218,7 @@ open problem: it is reliable on a clean screenshot and unreliable on a busy live
218
  `warning: no text was found on screen, so the index is 96 numbered grid cells and the
219
  agent is choosing blind` — instead of silently guessing.
220
 
221
- **153 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
222
  that click a real button.
223
 
224
  ## Verified on real Windows
@@ -260,7 +260,7 @@ the harness itself, installed into site-packages
260
 
261
  None of the three failures in the first row is a harness defect: two are
262
  `ModuleNotFoundError: numpy` and the third is the guard bug documented below. With the
263
- OCR engine installed, the same suite exits **0**. (Those Windows counts predate `trace.py`; the harness suite has since grown to 153 tests.)
264
 
265
  **The emulated-OCR caveat.** onnxruntime requires AVX2, and QEMU's software emulation of
266
  AVX2 is roughly two orders of magnitude slower than native. A suite that finishes in 17
@@ -286,7 +286,7 @@ exactly the kind of thing that only surfaces on a machine without an OCR engine.
286
 
287
  ## The test suite, in full
288
 
289
- `pytest tests` collects **153 tests**. 151 run headless — no display, no network, no
290
  weights. The last 2 are the opt-in live-desktop tests, skipped unless
291
  `TINY_COMPUTER_AGENT_LIVE_TEST=1`.
292
 
@@ -304,11 +304,13 @@ weights. The last 2 are the opt-in live-desktop tests, skipped unless
304
  | `tests/test_desktop.py` | 10 | display detection, the preflight checks, `doctor` |
305
  | `tests/test_cli.py` | 10 | the command line: `--doctor`, `--image`, `--live`, argument handling |
306
  | `tests/test_trace.py` | 28 | traces: a run recorded with no pixels, replayed against another brain, and scored against ground truth |
 
 
307
  | `tests/test_live_desktop.py` | 2 | **needs a display, opt-in**: OCR finds a real button at the pixels it was drawn at, and a real click makes the app react |
308
 
309
  ```bash
310
- pytest tests -q # 151 passed, 2 skipped
311
- TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 153 passed, on a desktop
312
  ```
313
 
314
  ## Install
@@ -317,7 +319,7 @@ TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 153 passed, on a desktop
317
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
318
  pip install -e ".[desktop,ocr]" # + real capture and real input
319
  pip install -e ".[vision,ocr]" # + the tiny model brains
320
- pytest # 153 tests, no display or network needed
321
  ```
322
 
323
  ## Run it on your machine
@@ -419,6 +421,605 @@ this: an answer file with non-canonical XML namespaces is **silently ignored** b
419
  | `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
420
  | `demo.py` | The synthetic desktop used by tests and `--image example` |
421
  | `trace.py` | Portable traces: record a run with no pixels, replay it offline, score the choice against ground truth |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  ## Why a tiny model can do this at all
424
 
@@ -473,6 +1074,8 @@ when no verifier is given.
473
  - [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
474
  - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
475
  - [x] Score episodes: replay a trace and count how often the choice was right - see [scoring a brain, offline](#scoring-a-brain-offline)
 
 
476
  - [ ] marimo console: live screen preview, action log, pause / resume / step
477
 
478
  ## License
 
24
 
25
  > **Status 0.3.0: it drives a real desktop — on Linux and on Windows.** Screen capture,
26
  > an OCR element index with real coordinates, a real mouse click, and a *verified*
27
+ > outcome. Every capability has a real screenshot in the [capability gallery](#capability-gallery), and a run can be saved as a portable *trace*, replayed offline and **scored**, so
28
  > "which brain is right, and how often?" is a number in a table.
29
  > The suite passes on a live Linux desktop and inside a real Windows Server
30
  > 2022 guest; what is not solved is model reliability — see
 
86
  ```bash
87
  python -m pip install -e ".[desktop,ocr,dev]"
88
  pytest tests -q
89
+ # 162 passed, 2 skipped in 18.7s
90
  ```
91
 
92
  The two skips are the live-desktop tests. Give them a display and they run for real:
 
218
  `warning: no text was found on screen, so the index is 96 numbered grid cells and the
219
  agent is choosing blind` — instead of silently guessing.
220
 
221
+ **164 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
222
  that click a real button.
223
 
224
  ## Verified on real Windows
 
260
 
261
  None of the three failures in the first row is a harness defect: two are
262
  `ModuleNotFoundError: numpy` and the third is the guard bug documented below. With the
263
+ OCR engine installed, the same suite exits **0**. (Those Windows counts predate `trace.py`; the harness suite has since grown to 164 tests.)
264
 
265
  **The emulated-OCR caveat.** onnxruntime requires AVX2, and QEMU's software emulation of
266
  AVX2 is roughly two orders of magnitude slower than native. A suite that finishes in 17
 
286
 
287
  ## The test suite, in full
288
 
289
+ `pytest tests` collects **164 tests**. 162 run headless — no display, no network, no
290
  weights. The last 2 are the opt-in live-desktop tests, skipped unless
291
  `TINY_COMPUTER_AGENT_LIVE_TEST=1`.
292
 
 
304
  | `tests/test_desktop.py` | 10 | display detection, the preflight checks, `doctor` |
305
  | `tests/test_cli.py` | 10 | the command line: `--doctor`, `--image`, `--live`, argument handling |
306
  | `tests/test_trace.py` | 28 | traces: a run recorded with no pixels, replayed against another brain, and scored against ground truth |
307
+ | `tests/test_models.py` | 7 | the model backends: lazy loading, device choice, honest refusals |
308
+ | `tests/test_api_surface.py` | 4 | the public API is a promise: everything advertised is importable, and nothing public is left out |
309
  | `tests/test_live_desktop.py` | 2 | **needs a display, opt-in**: OCR finds a real button at the pixels it was drawn at, and a real click makes the app react |
310
 
311
  ```bash
312
+ pytest tests -q # 162 passed, 2 skipped
313
+ TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 164 passed, on a desktop
314
  ```
315
 
316
  ## Install
 
319
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
320
  pip install -e ".[desktop,ocr]" # + real capture and real input
321
  pip install -e ".[vision,ocr]" # + the tiny model brains
322
+ pytest # 164 tests, no display or network needed
323
  ```
324
 
325
  ## Run it on your machine
 
421
  | `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
422
  | `demo.py` | The synthetic desktop used by tests and `--image example` |
423
  | `trace.py` | Portable traces: record a run with no pixels, replay it offline, score the choice against ground truth |
424
+ | `showcase.py` | The capability gallery: one real screenshot per capability, checked against the public API |
425
+
426
+ ## Capability gallery
427
+
428
+ **68 capabilities, one screenshot each - and every one of the 141 public callables in the package (functions, methods, classes and protocols) appears in at least one of them**, together with 6 of its public constants.
429
+
430
+ Nothing here is a mock-up. Each picture is produced by [`showcase.py`](showcase.py): a command or a call is run for real, and its actual output - or the real desktop it acted on - is what you see. The desktop shots are real screen captures on a real X server (Xvfb, 1280x800), including a real mouse click verified by the target application's own reaction.
431
+
432
+ > **An honest note on the platform.** These were captured on Linux, because the sandbox that builds this card has no Windows: no KVM, no QEMU and no Windows image. The harness is OS-agnostic (pyautogui uses `SendInput` on Windows, and `GrabScreen` falls back to Pillow's `ImageGrab`), so these are the same code paths - but they are not Windows pixels. The Windows twins are generated by the same script in [`.github/workflows/live-desktop.yml`](.github/workflows/live-desktop.yml).
433
+
434
+ Regenerate it with `python showcase.py` (`--list` to see the ids, `--only <id>` for a subset, `--card` to rewrite this section). It runs the calls for real, so a broken capability fails loudly instead of shipping a stale picture.
435
+
436
+ | area | capabilities |
437
+ |---|---:|
438
+ | Action vocabulary | 3 |
439
+ | Decisions | 7 |
440
+ | Execution | 2 |
441
+ | The loop | 7 |
442
+ | Safety | 4 |
443
+ | Screens | 6 |
444
+ | Perception | 6 |
445
+ | Preflight | 4 |
446
+ | Models | 5 |
447
+ | Traces & scoring | 7 |
448
+ | Command line | 9 |
449
+ | Real desktop | 7 |
450
+ | Extensibility | 1 |
451
+
452
+ ---
453
+
454
+ ### Action vocabulary
455
+
456
+ **The six verbs, and only six**
457
+
458
+ *Every action is validated on construction, before anything touches the machine.*
459
+
460
+ <img src="gallery/vocab-verbs.png" width="860">
461
+
462
+ `shows:` `ACTION_KINDS`, `ACTION_MENU`, `Action`, `Action.describe`, `Action.to_dict`, `parse_action`
463
+
464
+ **Malformed actions are refused, not guessed at**
465
+
466
+ *An unknown verb or a missing target raises before the machine is touched.*
467
+
468
+ <img src="gallery/vocab-refusals.png" width="860">
469
+
470
+ `shows:` `ActionError`, `Action`, `parse_action`
471
+
472
+ **The contract handed to a model**
473
+
474
+ *The prompt-side menu: one JSON object, six shapes, no invented verbs.*
475
+
476
+ <img src="gallery/vocab-menu.png" width="860">
477
+
478
+ `shows:` `ACTION_MENU`
479
+
480
+ ### Decisions
481
+
482
+ **A short numbered menu, in reading order**
483
+
484
+ *A 256M model cannot emit coordinates; it can pick a number from twelve.*
485
+
486
+ <img src="gallery/menu-numbered.png" width="860">
487
+
488
+ `shows:` `DecisionMenu`, `DecisionMenu.render`, `MenuItem`, `MenuItem.label`
489
+
490
+ **Menu lookup tolerates what models actually send**
491
+
492
+ *1, "2", 2.0 and "Save." all resolve; a stray word resolves to nothing.*
493
+
494
+ <img src="gallery/menu-lookup.png" width="860">
495
+
496
+ `shows:` `DecisionMenu.item`, `DecisionMenu.find`, `DecisionMenu.mentioned_in`
497
+
498
+ **Reading a decision out of a small model's reply**
499
+
500
+ *The LAST JSON object wins, because a model that echoes its prompt answers last.*
501
+
502
+ <img src="gallery/decision-json.png" width="860">
503
+
504
+ `shows:` `parse_decision`, `extract_json_object`, `extract_json_objects`, `DecisionMenu`
505
+
506
+ **An echoed prompt is refused; a right answer in the wrong field is recovered**
507
+
508
+ *Real 256M/0.5B failure modes, both caught: no blind click either way.*
509
+
510
+ <img src="gallery/decision-echo-guard.png" width="860">
511
+
512
+ `shows:` `parse_decision`, `BrainError`, `DecisionMenu.mentioned_in`
513
+
514
+ **A garbled verb with a valid label is still usable**
515
+
516
+ *A 256M model really does emit {"kind": "file", "label": "Save"}.*
517
+
518
+ <img src="gallery/decision-garbled.png" width="860">
519
+
520
+ `shows:` `parse_decision`, `BrainError`
521
+
522
+ **MockBrain: replay a fixed script, exactly once**
523
+
524
+ *Deterministic and offline - the workhorse for tests, demos and replays.*
525
+
526
+ <img src="gallery/brain-mock.png" width="860">
527
+
528
+ `shows:` `MockBrain`, `MockBrain.decide`, `MockBrain.remaining`
529
+
530
+ **PlannerBrain: no weights, no download, and it gives up honestly**
531
+
532
+ *The proof the harness works end to end, and a real fallback when no model is wanted.*
533
+
534
+ <img src="gallery/brain-planner.png" width="860">
535
+
536
+ `shows:` `PlannerBrain`, `PlannerBrain.decide`, `GoalUnreachable`
537
+
538
+ ### Execution
539
+
540
+ **Dry run resolves every click to pixels - and touches nothing**
541
+
542
+ *The default everywhere: read a whole episode before anything clicks.*
543
+
544
+ <img src="gallery/exec-dry-run.png" width="860">
545
+
546
+ `shows:` `DryRunExecutor`, `DryRunExecutor.execute`, `resolve_point`, `ActionResult`
547
+
548
+ **Real input refuses to guess a pixel for a label it cannot find**
549
+
550
+ *This is the gate between a hallucinated label and 'Delete account'.*
551
+
552
+ <img src="gallery/exec-refuses-unresolved.png" width="860">
553
+
554
+ `shows:` `ActionResult.summary`, `InputUnavailable`, `PyAutoGuiExecutor`, `PyAutoGuiExecutor.execute`
555
+
556
+ ### The loop
557
+
558
+ **observe -> think -> act -> verify, traced end to end**
559
+
560
+ *One call runs a whole episode; every step is kept for audit.*
561
+
562
+ <img src="gallery/loop-end-to-end.png" width="860">
563
+
564
+ `shows:` `AgentLoop`, `AgentLoop.run`, `Episode`, `Episode.render`, `Episode.to_rows`, `Step`, `Step.line`, `Step.ok`
565
+
566
+ **A hard step budget, so a run cannot wander**
567
+
568
+ *The budget ends the episode and says so; nothing executes past it.*
569
+
570
+ <img src="gallery/loop-budget.png" width="860">
571
+
572
+ `shows:` `Policy`, `Policy.step_budget`, `AgentLoop.run`
573
+
574
+ **Repeating one action against an unchanged screen is stopped**
575
+
576
+ *A stuck agent is not a persistent one - it is stopped and told why.*
577
+
578
+ <img src="gallery/loop-no-progress-guard.png" width="860">
579
+
580
+ `shows:` `AgentLoop.run`, `Step`, `Step.error`
581
+
582
+ **A kill-switch file aborts before the next action**
583
+
584
+ *Checked before every single action: create the file, the run stops.*
585
+
586
+ <img src="gallery/loop-kill-switch.png" width="860">
587
+
588
+ `shows:` `Policy.kill_switch_path`, `Policy.should_stop`, `AgentLoop.run`
589
+
590
+ **A dead screen or a broken brain is an episode, not a traceback**
591
+
592
+ *Every failure path is recorded as a failed step with a reason.*
593
+
594
+ <img src="gallery/loop-survives-failures.png" width="860">
595
+
596
+ `shows:` `AgentLoop.run`, `Episode.stop_reason`, `GoalUnreachable`, `BrainError`
597
+
598
+ **Success is evidence, not the model's word**
599
+
600
+ *Without a verifier, a successful action that removes its own target looks like a failure.*
601
+
602
+ <img src="gallery/loop-verifier.png" width="860">
603
+
604
+ `shows:` `AgentLoop.run`, `Episode.success`, `Observation.text`
605
+
606
+ **Every step can be streamed out as it happens**
607
+
608
+ *This is the hook a live console, a progress bar or a websocket uses.*
609
+
610
+ <img src="gallery/loop-on-step.png" width="860">
611
+
612
+ `shows:` `AgentLoop.run`, `Step.line`, `on_step`
613
+
614
+ ### Safety
615
+
616
+ **Typing from the deny list never reaches the keyboard**
617
+
618
+ *Checked before execution; a refusal ends the episode and clicks nothing.*
619
+
620
+ <img src="gallery/safety-deny-text.png" width="860">
621
+
622
+ `shows:` `Policy`, `Policy.check`, `PolicyViolation`, `DEFAULT_DENY_TEXT`
623
+
624
+ **Clicking a denied widget is refused, however the label is spelled**
625
+
626
+ *Terminal, keychain, wallet, password manager, bank - matched on the label.*
627
+
628
+ <img src="gallery/safety-deny-label.png" width="860">
629
+
630
+ `shows:` `Policy.deny_labels`, `Policy.check`, `Observation.find`
631
+
632
+ **Key chords on the deny list are never pressed**
633
+
634
+ *ctrl+alt+t, ctrl+alt+delete and ctrl+shift+escape are refused by default.*
635
+
636
+ <img src="gallery/safety-deny-keys.png" width="860">
637
+
638
+ `shows:` `Policy.deny_keys`, `Policy.check`, `PolicyViolation`
639
+
640
+ **A numbered grid cell is a blind click: allowed to plan, refused for real**
641
+
642
+ *With real input the blind guess is refused unless you pass --allow-grid.*
643
+
644
+ <img src="gallery/safety-blind-grid.png" width="860">
645
+
646
+ `shows:` `Policy.allow_grid_clicks`, `Policy.check`, `GridDetector`
647
+
648
+ ### Screens
649
+
650
+ **A scripted screen, for testing the loop with no display**
651
+
652
+ *The final frame repeats forever, so a runaway agent cannot exhaust the script.*
653
+
654
+ <img src="gallery/screen-fake.png" width="860">
655
+
656
+ `shows:` `FakeScreen`, `FakeScreen.capture`, `Observation`
657
+
658
+ **A still image (or a sequence) replayed as a screen**
659
+
660
+ *Lets the whole pipeline be exercised with no display, and a failure be replayed.*
661
+
662
+ <img src="gallery/screen-image.png" width="860">
663
+
664
+ `shows:` `ImageScreen`, `ImageScreen.capture`, `fake_desktop`
665
+
666
+ **Real capture: monitors, downscaling, and HiDPI mapping**
667
+
668
+ *Frame pixel -> screen pixel is preserved, so a click cannot drift.*
669
+
670
+ <img src="gallery/capture-options.png" width="860">
671
+
672
+ `shows:` `GrabScreen`, `GrabScreen.capture`, `GrabScreen.map_to_screen`, `ScreenUnavailable`
673
+
674
+ **Labels are compared on their alphanumeric core**
675
+
676
+ *OCR loses spaces, models add punctuation - so nothing matches by exact string.*
677
+
678
+ <img src="gallery/element-matching.png" width="860">
679
+
680
+ `shows:` `Element`, `Element.center`, `Element.matches`, `Observation.find`, `normalise_label`
681
+
682
+ **The compact text a model is shown**
683
+
684
+ *This is the whole world as the brain sees it - capped, and honest when empty.*
685
+
686
+ <img src="gallery/observation-summary.png" width="860">
687
+
688
+ `shows:` `Observation.summary`
689
+
690
+ **The built-in synthetic desktop**
691
+
692
+ *Draws a screenshot-like desktop offline - the same image --image example replays.*
693
+
694
+ <img src="gallery/fake-desktop.png" width="860">
695
+
696
+ `shows:` `fake_desktop`, `write_fake_desktop`, `OcrDetector`
697
+
698
+ ### Perception
699
+
700
+ **Probing for a detector, and building one by name**
701
+
702
+ *The doctor and the CLI both ask this before promising anything.*
703
+
704
+ <img src="gallery/detector-available.png" width="860">
705
+
706
+ `shows:` `detector_available`, `make_detector`, `DetectorUnavailable`, `Detector`
707
+
708
+ **OCR turns a screenshot into labelled, clickable elements**
709
+
710
+ *The model never has to guess where anything is: this is the grounding.*
711
+
712
+ <img src="gallery/ocr-labels.png" width="860">
713
+
714
+ `shows:` `OcrDetector`, `OcrDetector.detect`, `Element`
715
+
716
+ **Three knobs keep dialog prose out of the menu**
717
+
718
+ *Every extra line of noise is one more chance for a small model to click prose.*
719
+
720
+ <img src="gallery/ocr-filters.png" width="860">
721
+
722
+ `shows:` `OcrDetector.min_score`, `OcrDetector.max_label_chars`, `OcrDetector.min_label_chars`
723
+
724
+ **A numbered grid, for screens with no text at all**
725
+
726
+ *An agent with an empty index has no legal moves; a grid always has moves.*
727
+
728
+ <img src="gallery/grid-detector.png" width="860">
729
+
730
+ `shows:` `GridDetector`, `GridDetector.detect`
731
+
732
+ **OCR when there is text, a grid when there is none**
733
+
734
+ *The default: useful with nothing installed, better the moment OCR appears.*
735
+
736
+ <img src="gallery/auto-detector.png" width="860">
737
+
738
+ `shows:` `AutoDetector`, `AutoDetector.detect`
739
+
740
+ **IndexedScreen: capture + detect, composed**
741
+
742
+ *One capture() call gives the loop an index it can act on.*
743
+
744
+ <img src="gallery/indexed-screen.png" width="860">
745
+
746
+ `shows:` `IndexedScreen`, `IndexedScreen.capture`, `Observation.summary`
747
+
748
+ ### Preflight
749
+
750
+ **Is there a desktop session to drive at all?**
751
+
752
+ *The first question the CLI and the doctor both ask.*
753
+
754
+ <img src="gallery/display-available.png" width="860">
755
+
756
+ `shows:` `display_available`
757
+
758
+ **Every precondition, one list, each with the fix**
759
+
760
+ *Eight checks; a required one that fails is a blocking problem.*
761
+
762
+ <img src="gallery/run-checks.png" width="860">
763
+
764
+ `shows:` `run_checks`, `Check`, `Check.render`
765
+
766
+ **python -m tiny_computer_agent --doctor**
767
+
768
+ *The first thing to run on a new machine. Exits non-zero when not ready.*
769
+
770
+ <img src="gallery/cli-doctor.png" width="860">
771
+
772
+ `shows:` `detector_available`, `display_available`, `doctor`, `report`
773
+
774
+ **One call wires screen + detector + brain + executor + policy**
775
+
776
+ *Defaults are the safe ones: dry run, a step budget, and a kill-switch file.*
777
+
778
+ <img src="gallery/build-agent.png" width="860">
779
+
780
+ `shows:` `build_agent`, `Policy`, `AgentLoop`
781
+
782
+ ### Models
783
+
784
+ **Which model runtime is present, and are the weights cached?**
785
+
786
+ *Weights are loaded lazily, so asking is free.*
787
+
788
+ <img src="gallery/models-deps.png" width="860">
789
+
790
+ `shows:` `model_dependencies`, `model_cached`, `MODEL_BRAINS`, `make_model_brain`, `DEFAULT_TEXT_MODEL`, `DEFAULT_VISION_MODEL`, `BrainError`
791
+
792
+ **Device and dtype selection**
793
+
794
+ *CUDA if it is there, CPU otherwise; the harness never needs a GPU.*
795
+
796
+ <img src="gallery/models-device.png" width="860">
797
+
798
+ `shows:` `resolve_device`, `pick_dtype`
799
+
800
+ **The prompt a model actually receives - and how it learns from a dead end**
801
+
802
+ *Placeholders, not worked examples: a weak model echoes the prompt, and an echo fails validation.*
803
+
804
+ <img src="gallery/models-prompt.png" width="860">
805
+
806
+ `shows:` `ModelBrain`, `ModelBrain.build_prompt`, `ModelBrain.decide`
807
+
808
+ **A second decision is told what already failed**
809
+
810
+ *'you already tried this and the screen did NOT change' is the most useful line a small model gets.*
811
+
812
+ <img src="gallery/models-prompt-history.png" width="860">
813
+
814
+ `shows:` `ModelBrain`, `ModelBrain.build_prompt`, `ModelBrain.history`
815
+
816
+ **The two real model backends, and lazy loading**
817
+
818
+ *Constructing a brain costs nothing; the weights arrive on the first decide().*
819
+
820
+ <img src="gallery/models-swap.png" width="860">
821
+
822
+ `shows:` `TextPlannerBrain`, `VLMBrain`, `TextPlannerBrain.unload`, `VLMBrain.unload`, `ModelBrain.loaded`, `make_model_brain`
823
+
824
+ ### Traces & scoring
825
+
826
+ **A task with one right answer, and the frames to show**
827
+
828
+ *Ground truth lives with the task, so a run can be scored at all.*
829
+
830
+ <img src="gallery/scenario.png" width="860">
831
+
832
+ `shows:` `Scenario`, `Scenario.label`, `Scenario.screen`, `Trace.render`, `run_scenario`
833
+
834
+ **An episode becomes a portable, pixel-free trace**
835
+
836
+ *JSON, so it diffs in review and replays on a machine with nothing installed.*
837
+
838
+ <img src="gallery/trace-record.png" width="860">
839
+
840
+ `shows:` `Trace`, `Trace.blocked`, `Trace.choice`, `Trace.decided`, `Trace.decision_step`, `Trace.frames`, `Trace.load`, `Trace.name`, `Trace.record`, `Trace.save`, `Trace.to_dict`, `Trace.to_json`, `TraceStep`, `TraceStep.observation`, `TraceStep.to_dict`
841
+
842
+ **Replay a recorded trace against a different brain**
843
+
844
+ *The frames are the fixture; the brain is the only variable. Nothing is captured or clicked.*
845
+
846
+ <img src="gallery/trace-replay.png" width="860">
847
+
848
+ `shows:` `Trace.replay`, `Trace.observations`, `score_trace`
849
+
850
+ **Counting how often the choice was right**
851
+
852
+ *Scored on normalised text, so OCR spacing is forgiven - the same bargain the index makes.*
853
+
854
+ <img src="gallery/score-trace.png" width="860">
855
+
856
+ `shows:` `score_trace`, `score`, `score_replay`, `Verdict`, `Verdict.line`
857
+
858
+ **Verdicts collected into an accuracy**
859
+
860
+ *The number the whole module exists for: which brain is right, and how often.*
861
+
862
+ <img src="gallery/scoreboard.png" width="860">
863
+
864
+ `shows:` `Scoreboard`, `Scoreboard.accuracy`, `Scoreboard.accuracy_for`, `Scoreboard.add`, `Scoreboard.blocked`, `Scoreboard.brains`, `Scoreboard.by_brain`, `Scoreboard.confusion`, `Scoreboard.earned`, `Scoreboard.render`, `Scoreboard.to_rows`, `Scoreboard.total`
865
+
866
+ **Every scenario through every brain, in one table**
867
+
868
+ *A fresh brain per task, so state like 'already clicked this' cannot leak between them.*
869
+
870
+ <img src="gallery/benchmark.png" width="860">
871
+
872
+ `shows:` `benchmark`, `Scoreboard.to_rows`
873
+
874
+ **Replay a whole recorded suite against one brain**
875
+
876
+ *The literal roadmap step: the frames are fixed, nothing new is captured.*
877
+
878
+ <img src="gallery/benchmark-traces.png" width="860">
879
+
880
+ `shows:` `benchmark_traces`, `load_traces`, `Trace.save`
881
+
882
+ ### Command line
883
+
884
+ **--image example (replay a screenshot, dry run)**
885
+
886
+ *No display needed: the whole pipeline over a still image.*
887
+
888
+ <img src="gallery/cli-image.png" width="860">
889
+
890
+ `shows:` `main`, `build_parser`, `ImageScreen`, `detector_available`
891
+
892
+ **--script (replay a fixed action list instead of a brain)**
893
+
894
+ *The script is validated exactly like a model's answer: nothing skips the gates.*
895
+
896
+ <img src="gallery/cli-script.png" width="860">
897
+
898
+ `shows:` `MockBrain`, `demo_screen`, `parse_action`
899
+
900
+ **--verify-text (the harness decides success, not the brain)**
901
+
902
+ *Without it, an action that removes its own target looks like a failure.*
903
+
904
+ <img src="gallery/cli-verify-text.png" width="860">
905
+
906
+ `shows:` `AgentLoop.run`, `Episode.success`
907
+
908
+ **--trace / --expected (record the run, then score it)**
909
+
910
+ *Writes JSON next to the code, and prints the score straight away.*
911
+
912
+ <img src="gallery/cli-trace.png" width="860">
913
+
914
+ `shows:` `Trace.record`, `Trace.save`, `score_trace`
915
+
916
+ **--no-ocr (index a numbered grid instead)**
917
+
918
+ *Useful on a machine with no OCR engine, or for a canvas app.*
919
+
920
+ <img src="gallery/cli-detector.png" width="860">
921
+
922
+ `shows:` `GridDetector`, `make_detector`, `detector_available`
923
+
924
+ **Exit codes: 0 success, 1 failure, 2 bad usage, 3 no screen**
925
+
926
+ *A shell can tell 'it worked' from 'it gave up' without parsing any text.*
927
+
928
+ <img src="gallery/cli-exit-codes.png" width="860">
929
+
930
+ `shows:` `main`
931
+
932
+ **The two deliberate acts before the mouse can move**
933
+
934
+ *--real-input without the env var, and --brain vlm with no pixels: both refuse.*
935
+
936
+ <img src="gallery/cli-refusals.png" width="860">
937
+
938
+ `shows:` `main`, `ALLOW_REAL_INPUT_ENV`
939
+
940
+ **--live with no display is refused, with the fix**
941
+
942
+ *It tells you what to do instead of dying inside an episode.*
943
+
944
+ <img src="gallery/cli-live-headless.png" width="860">
945
+
946
+ `shows:` `display_available`, `main`
947
+
948
+ **--live (your real screen, dry run)**
949
+
950
+ *Real capture, real OCR, a real element index - and not one click.*
951
+
952
+ <img src="gallery/cli-live-dry-run.png" width="860">
953
+
954
+ `shows:` `GrabScreen`, `IndexedScreen`, `AutoDetector`, `main`
955
+
956
+ ### Real desktop
957
+
958
+ **GrabScreen: capture a real desktop**
959
+
960
+ *1280x800 of a real X server. mss first, Pillow's ImageGrab as the fallback.*
961
+
962
+ <img src="gallery/desk-capture.png" width="860">
963
+
964
+ `shows:` `GrabScreen`, `GrabScreen.capture`, `ScreenUnavailable`
965
+
966
+ **The element index, drawn on the pixels it came from**
967
+
968
+ *Every box is a real OCR word with a real screen coordinate: the model picks a label, not a pixel.*
969
+
970
+ <img src="gallery/desk-ocr-index.png" width="860">
971
+
972
+ `shows:` `OcrDetector`, `OcrDetector.detect`, `IndexedScreen`, `Element.center`
973
+
974
+ **A real click, and the window's own reaction**
975
+
976
+ *Pixels in, a label chosen, a pixel resolved, a button pressed - and the dialog closed itself.*
977
+
978
+ <img src="gallery/desk-click.png" width="860">
979
+
980
+ `shows:` `PyAutoGuiExecutor`, `AgentLoop.run`, `Policy.dry_run`, `resolve_point`
981
+
982
+ **Success only when the harness can see it**
983
+
984
+ *The app changed its own title and wrote a marker file; the verifier saw the change.*
985
+
986
+ <img src="gallery/desk-verify.png" width="860">
987
+
988
+ `shows:` `AgentLoop.run`, `Episode.success`, `Episode.stop_reason`
989
+
990
+ **A screen with no text still has legal moves**
991
+
992
+ *OCR finds nothing on a canvas, so the index degrades to numbered cells - and says so.*
993
+
994
+ <img src="gallery/desk-grid-fallback.png" width="860">
995
+
996
+ `shows:` `AutoDetector`, `GridDetector`, `OcrDetector`
997
+
998
+ **The safety gate, on a real button, in a real window**
999
+
1000
+ *The Terminal button is visible and labelled - and the policy refuses to press it.*
1001
+
1002
+ <img src="gallery/desk-policy-refusal.png" width="860">
1003
+
1004
+ `shows:` `Policy.check`, `Policy.deny_labels`, `PolicyViolation`, `Observation.find`
1005
+
1006
+ **The same click, driven end to end from the CLI**
1007
+
1008
+ *--live --real-input --verify-text Saved: capture, OCR, click, verify, exit code.*
1009
+
1010
+ <img src="gallery/desk-cli-real-input.png" width="860">
1011
+
1012
+ `shows:` `main`, `PyAutoGuiExecutor`, `AgentLoop.run`
1013
+
1014
+ ### Extensibility
1015
+
1016
+ **Every stage is a Protocol: bring your own screen, brain, detector, executor**
1017
+
1018
+ *The loop depends on four one-method contracts, not on the shipped implementations.*
1019
+
1020
+ <img src="gallery/protocols.png" width="860">
1021
+
1022
+ `shows:` `Brain`, `Brain.decide`, `ScreenSource`, `ScreenSource.capture`, `Detector`, `Detector.detect`, `Executor`, `Executor.execute`, `ActionResult`
1023
 
1024
  ## Why a tiny model can do this at all
1025
 
 
1074
  - [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
1075
  - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
1076
  - [x] Score episodes: replay a trace and count how often the choice was right - see [scoring a brain, offline](#scoring-a-brain-offline)
1077
+ - [x] A capability gallery: one real screenshot per capability, generated by `showcase.py`
1078
+ - [ ] A **Windows** run of the gallery: the workflow builds it on `windows-latest`, but it needs a GitHub mirror with Actions enabled - Hugging Face does not run GitHub Actions
1079
  - [ ] marimo console: live screen preview, action log, pause / resume / step
1080
 
1081
  ## License