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

Publish tiny_computer_agent_app.py from marimo

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