hkblockchaintechnology commited on
Commit
24516c4
·
verified ·
1 Parent(s): 669597a

Publish gallery/ from marimo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. gallery/README.md +602 -0
  2. gallery/_card_section.md +597 -0
  3. gallery/auto-detector.png +0 -0
  4. gallery/benchmark-traces.png +0 -0
  5. gallery/benchmark.png +0 -0
  6. gallery/brain-mock.png +0 -0
  7. gallery/brain-planner.png +0 -0
  8. gallery/build-agent.png +0 -0
  9. gallery/capture-options.png +0 -0
  10. gallery/cli-detector.png +0 -0
  11. gallery/cli-doctor.png +0 -0
  12. gallery/cli-exit-codes.png +0 -0
  13. gallery/cli-image.png +0 -0
  14. gallery/cli-live-dry-run.png +0 -0
  15. gallery/cli-live-headless.png +0 -0
  16. gallery/cli-refusals.png +0 -0
  17. gallery/cli-script.png +0 -0
  18. gallery/cli-trace.png +0 -0
  19. gallery/cli-verify-text.png +0 -0
  20. gallery/decision-echo-guard.png +0 -0
  21. gallery/decision-garbled.png +0 -0
  22. gallery/decision-json.png +0 -0
  23. gallery/desk-capture.png +0 -0
  24. gallery/desk-cli-real-input.png +0 -0
  25. gallery/desk-click.png +0 -0
  26. gallery/desk-grid-fallback.png +0 -0
  27. gallery/desk-ocr-index.png +0 -0
  28. gallery/desk-policy-refusal.png +0 -0
  29. gallery/desk-verify.png +0 -0
  30. gallery/detector-available.png +0 -0
  31. gallery/display-available.png +0 -0
  32. gallery/element-matching.png +0 -0
  33. gallery/exec-dry-run.png +0 -0
  34. gallery/exec-refuses-unresolved.png +0 -0
  35. gallery/fake-desktop.png +0 -0
  36. gallery/grid-detector.png +0 -0
  37. gallery/index.json +864 -0
  38. gallery/indexed-screen.png +0 -0
  39. gallery/loop-budget.png +0 -0
  40. gallery/loop-end-to-end.png +0 -0
  41. gallery/loop-kill-switch.png +0 -0
  42. gallery/loop-no-progress-guard.png +0 -0
  43. gallery/loop-on-step.png +0 -0
  44. gallery/loop-survives-failures.png +0 -0
  45. gallery/loop-verifier.png +0 -0
  46. gallery/menu-lookup.png +0 -0
  47. gallery/menu-numbered.png +0 -0
  48. gallery/models-deps.png +0 -0
  49. gallery/models-device.png +0 -0
  50. gallery/models-prompt-history.png +0 -0
gallery/README.md ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Capability gallery
3
+ ---
4
+
5
+ ## Capability gallery
6
+
7
+ **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.
8
+
9
+ 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.
10
+
11
+ > **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).
12
+
13
+ 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.
14
+
15
+ | area | capabilities |
16
+ |---|---:|
17
+ | Action vocabulary | 3 |
18
+ | Decisions | 7 |
19
+ | Execution | 2 |
20
+ | The loop | 7 |
21
+ | Safety | 4 |
22
+ | Screens | 6 |
23
+ | Perception | 6 |
24
+ | Preflight | 4 |
25
+ | Models | 5 |
26
+ | Traces & scoring | 7 |
27
+ | Command line | 9 |
28
+ | Real desktop | 7 |
29
+ | Extensibility | 1 |
30
+
31
+ ---
32
+
33
+ ### Action vocabulary
34
+
35
+ **The six verbs, and only six**
36
+
37
+ *Every action is validated on construction, before anything touches the machine.*
38
+
39
+ <img src="gallery/vocab-verbs.png" width="860">
40
+
41
+ `shows:` `ACTION_KINDS`, `ACTION_MENU`, `Action`, `Action.describe`, `Action.to_dict`, `parse_action`
42
+
43
+ **Malformed actions are refused, not guessed at**
44
+
45
+ *An unknown verb or a missing target raises before the machine is touched.*
46
+
47
+ <img src="gallery/vocab-refusals.png" width="860">
48
+
49
+ `shows:` `ActionError`, `Action`, `parse_action`
50
+
51
+ **The contract handed to a model**
52
+
53
+ *The prompt-side menu: one JSON object, six shapes, no invented verbs.*
54
+
55
+ <img src="gallery/vocab-menu.png" width="860">
56
+
57
+ `shows:` `ACTION_MENU`
58
+
59
+ ### Decisions
60
+
61
+ **A short numbered menu, in reading order**
62
+
63
+ *A 256M model cannot emit coordinates; it can pick a number from twelve.*
64
+
65
+ <img src="gallery/menu-numbered.png" width="860">
66
+
67
+ `shows:` `DecisionMenu`, `DecisionMenu.render`, `MenuItem`, `MenuItem.label`
68
+
69
+ **Menu lookup tolerates what models actually send**
70
+
71
+ *1, "2", 2.0 and "Save." all resolve; a stray word resolves to nothing.*
72
+
73
+ <img src="gallery/menu-lookup.png" width="860">
74
+
75
+ `shows:` `DecisionMenu.item`, `DecisionMenu.find`, `DecisionMenu.mentioned_in`
76
+
77
+ **Reading a decision out of a small model's reply**
78
+
79
+ *The LAST JSON object wins, because a model that echoes its prompt answers last.*
80
+
81
+ <img src="gallery/decision-json.png" width="860">
82
+
83
+ `shows:` `parse_decision`, `extract_json_object`, `extract_json_objects`, `DecisionMenu`
84
+
85
+ **An echoed prompt is refused; a right answer in the wrong field is recovered**
86
+
87
+ *Real 256M/0.5B failure modes, both caught: no blind click either way.*
88
+
89
+ <img src="gallery/decision-echo-guard.png" width="860">
90
+
91
+ `shows:` `parse_decision`, `BrainError`, `DecisionMenu.mentioned_in`
92
+
93
+ **A garbled verb with a valid label is still usable**
94
+
95
+ *A 256M model really does emit {"kind": "file", "label": "Save"}.*
96
+
97
+ <img src="gallery/decision-garbled.png" width="860">
98
+
99
+ `shows:` `parse_decision`, `BrainError`
100
+
101
+ **MockBrain: replay a fixed script, exactly once**
102
+
103
+ *Deterministic and offline - the workhorse for tests, demos and replays.*
104
+
105
+ <img src="gallery/brain-mock.png" width="860">
106
+
107
+ `shows:` `MockBrain`, `MockBrain.decide`, `MockBrain.remaining`
108
+
109
+ **PlannerBrain: no weights, no download, and it gives up honestly**
110
+
111
+ *The proof the harness works end to end, and a real fallback when no model is wanted.*
112
+
113
+ <img src="gallery/brain-planner.png" width="860">
114
+
115
+ `shows:` `PlannerBrain`, `PlannerBrain.decide`, `GoalUnreachable`
116
+
117
+ ### Execution
118
+
119
+ **Dry run resolves every click to pixels - and touches nothing**
120
+
121
+ *The default everywhere: read a whole episode before anything clicks.*
122
+
123
+ <img src="gallery/exec-dry-run.png" width="860">
124
+
125
+ `shows:` `DryRunExecutor`, `DryRunExecutor.execute`, `resolve_point`, `ActionResult`
126
+
127
+ **Real input refuses to guess a pixel for a label it cannot find**
128
+
129
+ *This is the gate between a hallucinated label and 'Delete account'.*
130
+
131
+ <img src="gallery/exec-refuses-unresolved.png" width="860">
132
+
133
+ `shows:` `ActionResult.summary`, `InputUnavailable`, `PyAutoGuiExecutor`, `PyAutoGuiExecutor.execute`
134
+
135
+ ### The loop
136
+
137
+ **observe -> think -> act -> verify, traced end to end**
138
+
139
+ *One call runs a whole episode; every step is kept for audit.*
140
+
141
+ <img src="gallery/loop-end-to-end.png" width="860">
142
+
143
+ `shows:` `AgentLoop`, `AgentLoop.run`, `Episode`, `Episode.render`, `Episode.to_rows`, `Step`, `Step.line`, `Step.ok`
144
+
145
+ **A hard step budget, so a run cannot wander**
146
+
147
+ *The budget ends the episode and says so; nothing executes past it.*
148
+
149
+ <img src="gallery/loop-budget.png" width="860">
150
+
151
+ `shows:` `Policy`, `Policy.step_budget`, `AgentLoop.run`
152
+
153
+ **Repeating one action against an unchanged screen is stopped**
154
+
155
+ *A stuck agent is not a persistent one - it is stopped and told why.*
156
+
157
+ <img src="gallery/loop-no-progress-guard.png" width="860">
158
+
159
+ `shows:` `AgentLoop.run`, `Step`, `Step.error`
160
+
161
+ **A kill-switch file aborts before the next action**
162
+
163
+ *Checked before every single action: create the file, the run stops.*
164
+
165
+ <img src="gallery/loop-kill-switch.png" width="860">
166
+
167
+ `shows:` `Policy.kill_switch_path`, `Policy.should_stop`, `AgentLoop.run`
168
+
169
+ **A dead screen or a broken brain is an episode, not a traceback**
170
+
171
+ *Every failure path is recorded as a failed step with a reason.*
172
+
173
+ <img src="gallery/loop-survives-failures.png" width="860">
174
+
175
+ `shows:` `AgentLoop.run`, `Episode.stop_reason`, `GoalUnreachable`, `BrainError`
176
+
177
+ **Success is evidence, not the model's word**
178
+
179
+ *Without a verifier, a successful action that removes its own target looks like a failure.*
180
+
181
+ <img src="gallery/loop-verifier.png" width="860">
182
+
183
+ `shows:` `AgentLoop.run`, `Episode.success`, `Observation.text`
184
+
185
+ **Every step can be streamed out as it happens**
186
+
187
+ *This is the hook a live console, a progress bar or a websocket uses.*
188
+
189
+ <img src="gallery/loop-on-step.png" width="860">
190
+
191
+ `shows:` `AgentLoop.run`, `Step.line`, `on_step`
192
+
193
+ ### Safety
194
+
195
+ **Typing from the deny list never reaches the keyboard**
196
+
197
+ *Checked before execution; a refusal ends the episode and clicks nothing.*
198
+
199
+ <img src="gallery/safety-deny-text.png" width="860">
200
+
201
+ `shows:` `Policy`, `Policy.check`, `PolicyViolation`, `DEFAULT_DENY_TEXT`
202
+
203
+ **Clicking a denied widget is refused, however the label is spelled**
204
+
205
+ *Terminal, keychain, wallet, password manager, bank - matched on the label.*
206
+
207
+ <img src="gallery/safety-deny-label.png" width="860">
208
+
209
+ `shows:` `Policy.deny_labels`, `Policy.check`, `Observation.find`
210
+
211
+ **Key chords on the deny list are never pressed**
212
+
213
+ *ctrl+alt+t, ctrl+alt+delete and ctrl+shift+escape are refused by default.*
214
+
215
+ <img src="gallery/safety-deny-keys.png" width="860">
216
+
217
+ `shows:` `Policy.deny_keys`, `Policy.check`, `PolicyViolation`
218
+
219
+ **A numbered grid cell is a blind click: allowed to plan, refused for real**
220
+
221
+ *With real input the blind guess is refused unless you pass --allow-grid.*
222
+
223
+ <img src="gallery/safety-blind-grid.png" width="860">
224
+
225
+ `shows:` `Policy.allow_grid_clicks`, `Policy.check`, `GridDetector`
226
+
227
+ ### Screens
228
+
229
+ **A scripted screen, for testing the loop with no display**
230
+
231
+ *The final frame repeats forever, so a runaway agent cannot exhaust the script.*
232
+
233
+ <img src="gallery/screen-fake.png" width="860">
234
+
235
+ `shows:` `FakeScreen`, `FakeScreen.capture`, `Observation`
236
+
237
+ **A still image (or a sequence) replayed as a screen**
238
+
239
+ *Lets the whole pipeline be exercised with no display, and a failure be replayed.*
240
+
241
+ <img src="gallery/screen-image.png" width="860">
242
+
243
+ `shows:` `ImageScreen`, `ImageScreen.capture`, `fake_desktop`
244
+
245
+ **Real capture: monitors, downscaling, and HiDPI mapping**
246
+
247
+ *Frame pixel -> screen pixel is preserved, so a click cannot drift.*
248
+
249
+ <img src="gallery/capture-options.png" width="860">
250
+
251
+ `shows:` `GrabScreen`, `GrabScreen.capture`, `GrabScreen.map_to_screen`, `ScreenUnavailable`
252
+
253
+ **Labels are compared on their alphanumeric core**
254
+
255
+ *OCR loses spaces, models add punctuation - so nothing matches by exact string.*
256
+
257
+ <img src="gallery/element-matching.png" width="860">
258
+
259
+ `shows:` `Element`, `Element.center`, `Element.matches`, `Observation.find`, `normalise_label`
260
+
261
+ **The compact text a model is shown**
262
+
263
+ *This is the whole world as the brain sees it - capped, and honest when empty.*
264
+
265
+ <img src="gallery/observation-summary.png" width="860">
266
+
267
+ `shows:` `Observation.summary`
268
+
269
+ **The built-in synthetic desktop**
270
+
271
+ *Draws a screenshot-like desktop offline - the same image --image example replays.*
272
+
273
+ <img src="gallery/fake-desktop.png" width="860">
274
+
275
+ `shows:` `fake_desktop`, `write_fake_desktop`, `OcrDetector`
276
+
277
+ ### Perception
278
+
279
+ **Probing for a detector, and building one by name**
280
+
281
+ *The doctor and the CLI both ask this before promising anything.*
282
+
283
+ <img src="gallery/detector-available.png" width="860">
284
+
285
+ `shows:` `detector_available`, `make_detector`, `DetectorUnavailable`, `Detector`
286
+
287
+ **OCR turns a screenshot into labelled, clickable elements**
288
+
289
+ *The model never has to guess where anything is: this is the grounding.*
290
+
291
+ <img src="gallery/ocr-labels.png" width="860">
292
+
293
+ `shows:` `OcrDetector`, `OcrDetector.detect`, `Element`
294
+
295
+ **Three knobs keep dialog prose out of the menu**
296
+
297
+ *Every extra line of noise is one more chance for a small model to click prose.*
298
+
299
+ <img src="gallery/ocr-filters.png" width="860">
300
+
301
+ `shows:` `OcrDetector.min_score`, `OcrDetector.max_label_chars`, `OcrDetector.min_label_chars`
302
+
303
+ **A numbered grid, for screens with no text at all**
304
+
305
+ *An agent with an empty index has no legal moves; a grid always has moves.*
306
+
307
+ <img src="gallery/grid-detector.png" width="860">
308
+
309
+ `shows:` `GridDetector`, `GridDetector.detect`
310
+
311
+ **OCR when there is text, a grid when there is none**
312
+
313
+ *The default: useful with nothing installed, better the moment OCR appears.*
314
+
315
+ <img src="gallery/auto-detector.png" width="860">
316
+
317
+ `shows:` `AutoDetector`, `AutoDetector.detect`
318
+
319
+ **IndexedScreen: capture + detect, composed**
320
+
321
+ *One capture() call gives the loop an index it can act on.*
322
+
323
+ <img src="gallery/indexed-screen.png" width="860">
324
+
325
+ `shows:` `IndexedScreen`, `IndexedScreen.capture`, `Observation.summary`
326
+
327
+ ### Preflight
328
+
329
+ **Is there a desktop session to drive at all?**
330
+
331
+ *The first question the CLI and the doctor both ask.*
332
+
333
+ <img src="gallery/display-available.png" width="860">
334
+
335
+ `shows:` `display_available`
336
+
337
+ **Every precondition, one list, each with the fix**
338
+
339
+ *Eight checks; a required one that fails is a blocking problem.*
340
+
341
+ <img src="gallery/run-checks.png" width="860">
342
+
343
+ `shows:` `run_checks`, `Check`, `Check.render`
344
+
345
+ **python -m tiny_computer_agent --doctor**
346
+
347
+ *The first thing to run on a new machine. Exits non-zero when not ready.*
348
+
349
+ <img src="gallery/cli-doctor.png" width="860">
350
+
351
+ `shows:` `detector_available`, `display_available`, `doctor`, `report`
352
+
353
+ **One call wires screen + detector + brain + executor + policy**
354
+
355
+ *Defaults are the safe ones: dry run, a step budget, and a kill-switch file.*
356
+
357
+ <img src="gallery/build-agent.png" width="860">
358
+
359
+ `shows:` `build_agent`, `Policy`, `AgentLoop`
360
+
361
+ ### Models
362
+
363
+ **Which model runtime is present, and are the weights cached?**
364
+
365
+ *Weights are loaded lazily, so asking is free.*
366
+
367
+ <img src="gallery/models-deps.png" width="860">
368
+
369
+ `shows:` `model_dependencies`, `model_cached`, `MODEL_BRAINS`, `make_model_brain`, `DEFAULT_TEXT_MODEL`, `DEFAULT_VISION_MODEL`, `BrainError`
370
+
371
+ **Device and dtype selection**
372
+
373
+ *CUDA if it is there, CPU otherwise; the harness never needs a GPU.*
374
+
375
+ <img src="gallery/models-device.png" width="860">
376
+
377
+ `shows:` `resolve_device`, `pick_dtype`
378
+
379
+ **The prompt a model actually receives - and how it learns from a dead end**
380
+
381
+ *Placeholders, not worked examples: a weak model echoes the prompt, and an echo fails validation.*
382
+
383
+ <img src="gallery/models-prompt.png" width="860">
384
+
385
+ `shows:` `ModelBrain`, `ModelBrain.build_prompt`, `ModelBrain.decide`
386
+
387
+ **A second decision is told what already failed**
388
+
389
+ *'you already tried this and the screen did NOT change' is the most useful line a small model gets.*
390
+
391
+ <img src="gallery/models-prompt-history.png" width="860">
392
+
393
+ `shows:` `ModelBrain`, `ModelBrain.build_prompt`, `ModelBrain.history`
394
+
395
+ **The two real model backends, and lazy loading**
396
+
397
+ *Constructing a brain costs nothing; the weights arrive on the first decide().*
398
+
399
+ <img src="gallery/models-swap.png" width="860">
400
+
401
+ `shows:` `TextPlannerBrain`, `VLMBrain`, `TextPlannerBrain.unload`, `VLMBrain.unload`, `ModelBrain.loaded`, `make_model_brain`
402
+
403
+ ### Traces & scoring
404
+
405
+ **A task with one right answer, and the frames to show**
406
+
407
+ *Ground truth lives with the task, so a run can be scored at all.*
408
+
409
+ <img src="gallery/scenario.png" width="860">
410
+
411
+ `shows:` `Scenario`, `Scenario.label`, `Scenario.screen`, `Trace.render`, `run_scenario`
412
+
413
+ **An episode becomes a portable, pixel-free trace**
414
+
415
+ *JSON, so it diffs in review and replays on a machine with nothing installed.*
416
+
417
+ <img src="gallery/trace-record.png" width="860">
418
+
419
+ `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`
420
+
421
+ **Replay a recorded trace against a different brain**
422
+
423
+ *The frames are the fixture; the brain is the only variable. Nothing is captured or clicked.*
424
+
425
+ <img src="gallery/trace-replay.png" width="860">
426
+
427
+ `shows:` `Trace.replay`, `Trace.observations`, `score_trace`
428
+
429
+ **Counting how often the choice was right**
430
+
431
+ *Scored on normalised text, so OCR spacing is forgiven - the same bargain the index makes.*
432
+
433
+ <img src="gallery/score-trace.png" width="860">
434
+
435
+ `shows:` `score_trace`, `score`, `score_replay`, `Verdict`, `Verdict.line`
436
+
437
+ **Verdicts collected into an accuracy**
438
+
439
+ *The number the whole module exists for: which brain is right, and how often.*
440
+
441
+ <img src="gallery/scoreboard.png" width="860">
442
+
443
+ `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`
444
+
445
+ **Every scenario through every brain, in one table**
446
+
447
+ *A fresh brain per task, so state like 'already clicked this' cannot leak between them.*
448
+
449
+ <img src="gallery/benchmark.png" width="860">
450
+
451
+ `shows:` `benchmark`, `Scoreboard.to_rows`
452
+
453
+ **Replay a whole recorded suite against one brain**
454
+
455
+ *The literal roadmap step: the frames are fixed, nothing new is captured.*
456
+
457
+ <img src="gallery/benchmark-traces.png" width="860">
458
+
459
+ `shows:` `benchmark_traces`, `load_traces`, `Trace.save`
460
+
461
+ ### Command line
462
+
463
+ **--image example (replay a screenshot, dry run)**
464
+
465
+ *No display needed: the whole pipeline over a still image.*
466
+
467
+ <img src="gallery/cli-image.png" width="860">
468
+
469
+ `shows:` `main`, `build_parser`, `ImageScreen`, `detector_available`
470
+
471
+ **--script (replay a fixed action list instead of a brain)**
472
+
473
+ *The script is validated exactly like a model's answer: nothing skips the gates.*
474
+
475
+ <img src="gallery/cli-script.png" width="860">
476
+
477
+ `shows:` `MockBrain`, `demo_screen`, `parse_action`
478
+
479
+ **--verify-text (the harness decides success, not the brain)**
480
+
481
+ *Without it, an action that removes its own target looks like a failure.*
482
+
483
+ <img src="gallery/cli-verify-text.png" width="860">
484
+
485
+ `shows:` `AgentLoop.run`, `Episode.success`
486
+
487
+ **--trace / --expected (record the run, then score it)**
488
+
489
+ *Writes JSON next to the code, and prints the score straight away.*
490
+
491
+ <img src="gallery/cli-trace.png" width="860">
492
+
493
+ `shows:` `Trace.record`, `Trace.save`, `score_trace`
494
+
495
+ **--no-ocr (index a numbered grid instead)**
496
+
497
+ *Useful on a machine with no OCR engine, or for a canvas app.*
498
+
499
+ <img src="gallery/cli-detector.png" width="860">
500
+
501
+ `shows:` `GridDetector`, `make_detector`, `detector_available`
502
+
503
+ **Exit codes: 0 success, 1 failure, 2 bad usage, 3 no screen**
504
+
505
+ *A shell can tell 'it worked' from 'it gave up' without parsing any text.*
506
+
507
+ <img src="gallery/cli-exit-codes.png" width="860">
508
+
509
+ `shows:` `main`
510
+
511
+ **The two deliberate acts before the mouse can move**
512
+
513
+ *--real-input without the env var, and --brain vlm with no pixels: both refuse.*
514
+
515
+ <img src="gallery/cli-refusals.png" width="860">
516
+
517
+ `shows:` `main`, `ALLOW_REAL_INPUT_ENV`
518
+
519
+ **--live with no display is refused, with the fix**
520
+
521
+ *It tells you what to do instead of dying inside an episode.*
522
+
523
+ <img src="gallery/cli-live-headless.png" width="860">
524
+
525
+ `shows:` `display_available`, `main`
526
+
527
+ **--live (your real screen, dry run)**
528
+
529
+ *Real capture, real OCR, a real element index - and not one click.*
530
+
531
+ <img src="gallery/cli-live-dry-run.png" width="860">
532
+
533
+ `shows:` `GrabScreen`, `IndexedScreen`, `AutoDetector`, `main`
534
+
535
+ ### Real desktop
536
+
537
+ **GrabScreen: capture a real desktop**
538
+
539
+ *1280x800 of a real X server. mss first, Pillow's ImageGrab as the fallback.*
540
+
541
+ <img src="gallery/desk-capture.png" width="860">
542
+
543
+ `shows:` `GrabScreen`, `GrabScreen.capture`, `ScreenUnavailable`
544
+
545
+ **The element index, drawn on the pixels it came from**
546
+
547
+ *Every box is a real OCR word with a real screen coordinate: the model picks a label, not a pixel.*
548
+
549
+ <img src="gallery/desk-ocr-index.png" width="860">
550
+
551
+ `shows:` `OcrDetector`, `OcrDetector.detect`, `IndexedScreen`, `Element.center`
552
+
553
+ **A real click, and the window's own reaction**
554
+
555
+ *Pixels in, a label chosen, a pixel resolved, a button pressed - and the dialog closed itself.*
556
+
557
+ <img src="gallery/desk-click.png" width="860">
558
+
559
+ `shows:` `PyAutoGuiExecutor`, `AgentLoop.run`, `Policy.dry_run`, `resolve_point`
560
+
561
+ **Success only when the harness can see it**
562
+
563
+ *The app changed its own title and wrote a marker file; the verifier saw the change.*
564
+
565
+ <img src="gallery/desk-verify.png" width="860">
566
+
567
+ `shows:` `AgentLoop.run`, `Episode.success`, `Episode.stop_reason`
568
+
569
+ **A screen with no text still has legal moves**
570
+
571
+ *OCR finds nothing on a canvas, so the index degrades to numbered cells - and says so.*
572
+
573
+ <img src="gallery/desk-grid-fallback.png" width="860">
574
+
575
+ `shows:` `AutoDetector`, `GridDetector`, `OcrDetector`
576
+
577
+ **The safety gate, on a real button, in a real window**
578
+
579
+ *The Terminal button is visible and labelled - and the policy refuses to press it.*
580
+
581
+ <img src="gallery/desk-policy-refusal.png" width="860">
582
+
583
+ `shows:` `Policy.check`, `Policy.deny_labels`, `PolicyViolation`, `Observation.find`
584
+
585
+ **The same click, driven end to end from the CLI**
586
+
587
+ *--live --real-input --verify-text Saved: capture, OCR, click, verify, exit code.*
588
+
589
+ <img src="gallery/desk-cli-real-input.png" width="860">
590
+
591
+ `shows:` `main`, `PyAutoGuiExecutor`, `AgentLoop.run`
592
+
593
+ ### Extensibility
594
+
595
+ **Every stage is a Protocol: bring your own screen, brain, detector, executor**
596
+
597
+ *The loop depends on four one-method contracts, not on the shipped implementations.*
598
+
599
+ <img src="gallery/protocols.png" width="860">
600
+
601
+ `shows:` `Brain`, `Brain.decide`, `ScreenSource`, `ScreenSource.capture`, `Detector`, `Detector.detect`, `Executor`, `Executor.execute`, `ActionResult`
602
+
gallery/_card_section.md ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Capability gallery
2
+
3
+ **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.
4
+
5
+ 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.
6
+
7
+ > **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).
8
+
9
+ 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.
10
+
11
+ | area | capabilities |
12
+ |---|---:|
13
+ | Action vocabulary | 3 |
14
+ | Decisions | 7 |
15
+ | Execution | 2 |
16
+ | The loop | 7 |
17
+ | Safety | 4 |
18
+ | Screens | 6 |
19
+ | Perception | 6 |
20
+ | Preflight | 4 |
21
+ | Models | 5 |
22
+ | Traces & scoring | 7 |
23
+ | Command line | 9 |
24
+ | Real desktop | 7 |
25
+ | Extensibility | 1 |
26
+
27
+ ---
28
+
29
+ ### Action vocabulary
30
+
31
+ **The six verbs, and only six**
32
+
33
+ *Every action is validated on construction, before anything touches the machine.*
34
+
35
+ <img src="gallery/vocab-verbs.png" width="860">
36
+
37
+ `shows:` `ACTION_KINDS`, `ACTION_MENU`, `Action`, `Action.describe`, `Action.to_dict`, `parse_action`
38
+
39
+ **Malformed actions are refused, not guessed at**
40
+
41
+ *An unknown verb or a missing target raises before the machine is touched.*
42
+
43
+ <img src="gallery/vocab-refusals.png" width="860">
44
+
45
+ `shows:` `ActionError`, `Action`, `parse_action`
46
+
47
+ **The contract handed to a model**
48
+
49
+ *The prompt-side menu: one JSON object, six shapes, no invented verbs.*
50
+
51
+ <img src="gallery/vocab-menu.png" width="860">
52
+
53
+ `shows:` `ACTION_MENU`
54
+
55
+ ### Decisions
56
+
57
+ **A short numbered menu, in reading order**
58
+
59
+ *A 256M model cannot emit coordinates; it can pick a number from twelve.*
60
+
61
+ <img src="gallery/menu-numbered.png" width="860">
62
+
63
+ `shows:` `DecisionMenu`, `DecisionMenu.render`, `MenuItem`, `MenuItem.label`
64
+
65
+ **Menu lookup tolerates what models actually send**
66
+
67
+ *1, "2", 2.0 and "Save." all resolve; a stray word resolves to nothing.*
68
+
69
+ <img src="gallery/menu-lookup.png" width="860">
70
+
71
+ `shows:` `DecisionMenu.item`, `DecisionMenu.find`, `DecisionMenu.mentioned_in`
72
+
73
+ **Reading a decision out of a small model's reply**
74
+
75
+ *The LAST JSON object wins, because a model that echoes its prompt answers last.*
76
+
77
+ <img src="gallery/decision-json.png" width="860">
78
+
79
+ `shows:` `parse_decision`, `extract_json_object`, `extract_json_objects`, `DecisionMenu`
80
+
81
+ **An echoed prompt is refused; a right answer in the wrong field is recovered**
82
+
83
+ *Real 256M/0.5B failure modes, both caught: no blind click either way.*
84
+
85
+ <img src="gallery/decision-echo-guard.png" width="860">
86
+
87
+ `shows:` `parse_decision`, `BrainError`, `DecisionMenu.mentioned_in`
88
+
89
+ **A garbled verb with a valid label is still usable**
90
+
91
+ *A 256M model really does emit {"kind": "file", "label": "Save"}.*
92
+
93
+ <img src="gallery/decision-garbled.png" width="860">
94
+
95
+ `shows:` `parse_decision`, `BrainError`
96
+
97
+ **MockBrain: replay a fixed script, exactly once**
98
+
99
+ *Deterministic and offline - the workhorse for tests, demos and replays.*
100
+
101
+ <img src="gallery/brain-mock.png" width="860">
102
+
103
+ `shows:` `MockBrain`, `MockBrain.decide`, `MockBrain.remaining`
104
+
105
+ **PlannerBrain: no weights, no download, and it gives up honestly**
106
+
107
+ *The proof the harness works end to end, and a real fallback when no model is wanted.*
108
+
109
+ <img src="gallery/brain-planner.png" width="860">
110
+
111
+ `shows:` `PlannerBrain`, `PlannerBrain.decide`, `GoalUnreachable`
112
+
113
+ ### Execution
114
+
115
+ **Dry run resolves every click to pixels - and touches nothing**
116
+
117
+ *The default everywhere: read a whole episode before anything clicks.*
118
+
119
+ <img src="gallery/exec-dry-run.png" width="860">
120
+
121
+ `shows:` `DryRunExecutor`, `DryRunExecutor.execute`, `resolve_point`, `ActionResult`
122
+
123
+ **Real input refuses to guess a pixel for a label it cannot find**
124
+
125
+ *This is the gate between a hallucinated label and 'Delete account'.*
126
+
127
+ <img src="gallery/exec-refuses-unresolved.png" width="860">
128
+
129
+ `shows:` `ActionResult.summary`, `InputUnavailable`, `PyAutoGuiExecutor`, `PyAutoGuiExecutor.execute`
130
+
131
+ ### The loop
132
+
133
+ **observe -> think -> act -> verify, traced end to end**
134
+
135
+ *One call runs a whole episode; every step is kept for audit.*
136
+
137
+ <img src="gallery/loop-end-to-end.png" width="860">
138
+
139
+ `shows:` `AgentLoop`, `AgentLoop.run`, `Episode`, `Episode.render`, `Episode.to_rows`, `Step`, `Step.line`, `Step.ok`
140
+
141
+ **A hard step budget, so a run cannot wander**
142
+
143
+ *The budget ends the episode and says so; nothing executes past it.*
144
+
145
+ <img src="gallery/loop-budget.png" width="860">
146
+
147
+ `shows:` `Policy`, `Policy.step_budget`, `AgentLoop.run`
148
+
149
+ **Repeating one action against an unchanged screen is stopped**
150
+
151
+ *A stuck agent is not a persistent one - it is stopped and told why.*
152
+
153
+ <img src="gallery/loop-no-progress-guard.png" width="860">
154
+
155
+ `shows:` `AgentLoop.run`, `Step`, `Step.error`
156
+
157
+ **A kill-switch file aborts before the next action**
158
+
159
+ *Checked before every single action: create the file, the run stops.*
160
+
161
+ <img src="gallery/loop-kill-switch.png" width="860">
162
+
163
+ `shows:` `Policy.kill_switch_path`, `Policy.should_stop`, `AgentLoop.run`
164
+
165
+ **A dead screen or a broken brain is an episode, not a traceback**
166
+
167
+ *Every failure path is recorded as a failed step with a reason.*
168
+
169
+ <img src="gallery/loop-survives-failures.png" width="860">
170
+
171
+ `shows:` `AgentLoop.run`, `Episode.stop_reason`, `GoalUnreachable`, `BrainError`
172
+
173
+ **Success is evidence, not the model's word**
174
+
175
+ *Without a verifier, a successful action that removes its own target looks like a failure.*
176
+
177
+ <img src="gallery/loop-verifier.png" width="860">
178
+
179
+ `shows:` `AgentLoop.run`, `Episode.success`, `Observation.text`
180
+
181
+ **Every step can be streamed out as it happens**
182
+
183
+ *This is the hook a live console, a progress bar or a websocket uses.*
184
+
185
+ <img src="gallery/loop-on-step.png" width="860">
186
+
187
+ `shows:` `AgentLoop.run`, `Step.line`, `on_step`
188
+
189
+ ### Safety
190
+
191
+ **Typing from the deny list never reaches the keyboard**
192
+
193
+ *Checked before execution; a refusal ends the episode and clicks nothing.*
194
+
195
+ <img src="gallery/safety-deny-text.png" width="860">
196
+
197
+ `shows:` `Policy`, `Policy.check`, `PolicyViolation`, `DEFAULT_DENY_TEXT`
198
+
199
+ **Clicking a denied widget is refused, however the label is spelled**
200
+
201
+ *Terminal, keychain, wallet, password manager, bank - matched on the label.*
202
+
203
+ <img src="gallery/safety-deny-label.png" width="860">
204
+
205
+ `shows:` `Policy.deny_labels`, `Policy.check`, `Observation.find`
206
+
207
+ **Key chords on the deny list are never pressed**
208
+
209
+ *ctrl+alt+t, ctrl+alt+delete and ctrl+shift+escape are refused by default.*
210
+
211
+ <img src="gallery/safety-deny-keys.png" width="860">
212
+
213
+ `shows:` `Policy.deny_keys`, `Policy.check`, `PolicyViolation`
214
+
215
+ **A numbered grid cell is a blind click: allowed to plan, refused for real**
216
+
217
+ *With real input the blind guess is refused unless you pass --allow-grid.*
218
+
219
+ <img src="gallery/safety-blind-grid.png" width="860">
220
+
221
+ `shows:` `Policy.allow_grid_clicks`, `Policy.check`, `GridDetector`
222
+
223
+ ### Screens
224
+
225
+ **A scripted screen, for testing the loop with no display**
226
+
227
+ *The final frame repeats forever, so a runaway agent cannot exhaust the script.*
228
+
229
+ <img src="gallery/screen-fake.png" width="860">
230
+
231
+ `shows:` `FakeScreen`, `FakeScreen.capture`, `Observation`
232
+
233
+ **A still image (or a sequence) replayed as a screen**
234
+
235
+ *Lets the whole pipeline be exercised with no display, and a failure be replayed.*
236
+
237
+ <img src="gallery/screen-image.png" width="860">
238
+
239
+ `shows:` `ImageScreen`, `ImageScreen.capture`, `fake_desktop`
240
+
241
+ **Real capture: monitors, downscaling, and HiDPI mapping**
242
+
243
+ *Frame pixel -> screen pixel is preserved, so a click cannot drift.*
244
+
245
+ <img src="gallery/capture-options.png" width="860">
246
+
247
+ `shows:` `GrabScreen`, `GrabScreen.capture`, `GrabScreen.map_to_screen`, `ScreenUnavailable`
248
+
249
+ **Labels are compared on their alphanumeric core**
250
+
251
+ *OCR loses spaces, models add punctuation - so nothing matches by exact string.*
252
+
253
+ <img src="gallery/element-matching.png" width="860">
254
+
255
+ `shows:` `Element`, `Element.center`, `Element.matches`, `Observation.find`, `normalise_label`
256
+
257
+ **The compact text a model is shown**
258
+
259
+ *This is the whole world as the brain sees it - capped, and honest when empty.*
260
+
261
+ <img src="gallery/observation-summary.png" width="860">
262
+
263
+ `shows:` `Observation.summary`
264
+
265
+ **The built-in synthetic desktop**
266
+
267
+ *Draws a screenshot-like desktop offline - the same image --image example replays.*
268
+
269
+ <img src="gallery/fake-desktop.png" width="860">
270
+
271
+ `shows:` `fake_desktop`, `write_fake_desktop`, `OcrDetector`
272
+
273
+ ### Perception
274
+
275
+ **Probing for a detector, and building one by name**
276
+
277
+ *The doctor and the CLI both ask this before promising anything.*
278
+
279
+ <img src="gallery/detector-available.png" width="860">
280
+
281
+ `shows:` `detector_available`, `make_detector`, `DetectorUnavailable`, `Detector`
282
+
283
+ **OCR turns a screenshot into labelled, clickable elements**
284
+
285
+ *The model never has to guess where anything is: this is the grounding.*
286
+
287
+ <img src="gallery/ocr-labels.png" width="860">
288
+
289
+ `shows:` `OcrDetector`, `OcrDetector.detect`, `Element`
290
+
291
+ **Three knobs keep dialog prose out of the menu**
292
+
293
+ *Every extra line of noise is one more chance for a small model to click prose.*
294
+
295
+ <img src="gallery/ocr-filters.png" width="860">
296
+
297
+ `shows:` `OcrDetector.min_score`, `OcrDetector.max_label_chars`, `OcrDetector.min_label_chars`
298
+
299
+ **A numbered grid, for screens with no text at all**
300
+
301
+ *An agent with an empty index has no legal moves; a grid always has moves.*
302
+
303
+ <img src="gallery/grid-detector.png" width="860">
304
+
305
+ `shows:` `GridDetector`, `GridDetector.detect`
306
+
307
+ **OCR when there is text, a grid when there is none**
308
+
309
+ *The default: useful with nothing installed, better the moment OCR appears.*
310
+
311
+ <img src="gallery/auto-detector.png" width="860">
312
+
313
+ `shows:` `AutoDetector`, `AutoDetector.detect`
314
+
315
+ **IndexedScreen: capture + detect, composed**
316
+
317
+ *One capture() call gives the loop an index it can act on.*
318
+
319
+ <img src="gallery/indexed-screen.png" width="860">
320
+
321
+ `shows:` `IndexedScreen`, `IndexedScreen.capture`, `Observation.summary`
322
+
323
+ ### Preflight
324
+
325
+ **Is there a desktop session to drive at all?**
326
+
327
+ *The first question the CLI and the doctor both ask.*
328
+
329
+ <img src="gallery/display-available.png" width="860">
330
+
331
+ `shows:` `display_available`
332
+
333
+ **Every precondition, one list, each with the fix**
334
+
335
+ *Eight checks; a required one that fails is a blocking problem.*
336
+
337
+ <img src="gallery/run-checks.png" width="860">
338
+
339
+ `shows:` `run_checks`, `Check`, `Check.render`
340
+
341
+ **python -m tiny_computer_agent --doctor**
342
+
343
+ *The first thing to run on a new machine. Exits non-zero when not ready.*
344
+
345
+ <img src="gallery/cli-doctor.png" width="860">
346
+
347
+ `shows:` `detector_available`, `display_available`, `doctor`, `report`
348
+
349
+ **One call wires screen + detector + brain + executor + policy**
350
+
351
+ *Defaults are the safe ones: dry run, a step budget, and a kill-switch file.*
352
+
353
+ <img src="gallery/build-agent.png" width="860">
354
+
355
+ `shows:` `build_agent`, `Policy`, `AgentLoop`
356
+
357
+ ### Models
358
+
359
+ **Which model runtime is present, and are the weights cached?**
360
+
361
+ *Weights are loaded lazily, so asking is free.*
362
+
363
+ <img src="gallery/models-deps.png" width="860">
364
+
365
+ `shows:` `model_dependencies`, `model_cached`, `MODEL_BRAINS`, `make_model_brain`, `DEFAULT_TEXT_MODEL`, `DEFAULT_VISION_MODEL`, `BrainError`
366
+
367
+ **Device and dtype selection**
368
+
369
+ *CUDA if it is there, CPU otherwise; the harness never needs a GPU.*
370
+
371
+ <img src="gallery/models-device.png" width="860">
372
+
373
+ `shows:` `resolve_device`, `pick_dtype`
374
+
375
+ **The prompt a model actually receives - and how it learns from a dead end**
376
+
377
+ *Placeholders, not worked examples: a weak model echoes the prompt, and an echo fails validation.*
378
+
379
+ <img src="gallery/models-prompt.png" width="860">
380
+
381
+ `shows:` `ModelBrain`, `ModelBrain.build_prompt`, `ModelBrain.decide`
382
+
383
+ **A second decision is told what already failed**
384
+
385
+ *'you already tried this and the screen did NOT change' is the most useful line a small model gets.*
386
+
387
+ <img src="gallery/models-prompt-history.png" width="860">
388
+
389
+ `shows:` `ModelBrain`, `ModelBrain.build_prompt`, `ModelBrain.history`
390
+
391
+ **The two real model backends, and lazy loading**
392
+
393
+ *Constructing a brain costs nothing; the weights arrive on the first decide().*
394
+
395
+ <img src="gallery/models-swap.png" width="860">
396
+
397
+ `shows:` `TextPlannerBrain`, `VLMBrain`, `TextPlannerBrain.unload`, `VLMBrain.unload`, `ModelBrain.loaded`, `make_model_brain`
398
+
399
+ ### Traces & scoring
400
+
401
+ **A task with one right answer, and the frames to show**
402
+
403
+ *Ground truth lives with the task, so a run can be scored at all.*
404
+
405
+ <img src="gallery/scenario.png" width="860">
406
+
407
+ `shows:` `Scenario`, `Scenario.label`, `Scenario.screen`, `Trace.render`, `run_scenario`
408
+
409
+ **An episode becomes a portable, pixel-free trace**
410
+
411
+ *JSON, so it diffs in review and replays on a machine with nothing installed.*
412
+
413
+ <img src="gallery/trace-record.png" width="860">
414
+
415
+ `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`
416
+
417
+ **Replay a recorded trace against a different brain**
418
+
419
+ *The frames are the fixture; the brain is the only variable. Nothing is captured or clicked.*
420
+
421
+ <img src="gallery/trace-replay.png" width="860">
422
+
423
+ `shows:` `Trace.replay`, `Trace.observations`, `score_trace`
424
+
425
+ **Counting how often the choice was right**
426
+
427
+ *Scored on normalised text, so OCR spacing is forgiven - the same bargain the index makes.*
428
+
429
+ <img src="gallery/score-trace.png" width="860">
430
+
431
+ `shows:` `score_trace`, `score`, `score_replay`, `Verdict`, `Verdict.line`
432
+
433
+ **Verdicts collected into an accuracy**
434
+
435
+ *The number the whole module exists for: which brain is right, and how often.*
436
+
437
+ <img src="gallery/scoreboard.png" width="860">
438
+
439
+ `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`
440
+
441
+ **Every scenario through every brain, in one table**
442
+
443
+ *A fresh brain per task, so state like 'already clicked this' cannot leak between them.*
444
+
445
+ <img src="gallery/benchmark.png" width="860">
446
+
447
+ `shows:` `benchmark`, `Scoreboard.to_rows`
448
+
449
+ **Replay a whole recorded suite against one brain**
450
+
451
+ *The literal roadmap step: the frames are fixed, nothing new is captured.*
452
+
453
+ <img src="gallery/benchmark-traces.png" width="860">
454
+
455
+ `shows:` `benchmark_traces`, `load_traces`, `Trace.save`
456
+
457
+ ### Command line
458
+
459
+ **--image example (replay a screenshot, dry run)**
460
+
461
+ *No display needed: the whole pipeline over a still image.*
462
+
463
+ <img src="gallery/cli-image.png" width="860">
464
+
465
+ `shows:` `main`, `build_parser`, `ImageScreen`, `detector_available`
466
+
467
+ **--script (replay a fixed action list instead of a brain)**
468
+
469
+ *The script is validated exactly like a model's answer: nothing skips the gates.*
470
+
471
+ <img src="gallery/cli-script.png" width="860">
472
+
473
+ `shows:` `MockBrain`, `demo_screen`, `parse_action`
474
+
475
+ **--verify-text (the harness decides success, not the brain)**
476
+
477
+ *Without it, an action that removes its own target looks like a failure.*
478
+
479
+ <img src="gallery/cli-verify-text.png" width="860">
480
+
481
+ `shows:` `AgentLoop.run`, `Episode.success`
482
+
483
+ **--trace / --expected (record the run, then score it)**
484
+
485
+ *Writes JSON next to the code, and prints the score straight away.*
486
+
487
+ <img src="gallery/cli-trace.png" width="860">
488
+
489
+ `shows:` `Trace.record`, `Trace.save`, `score_trace`
490
+
491
+ **--no-ocr (index a numbered grid instead)**
492
+
493
+ *Useful on a machine with no OCR engine, or for a canvas app.*
494
+
495
+ <img src="gallery/cli-detector.png" width="860">
496
+
497
+ `shows:` `GridDetector`, `make_detector`, `detector_available`
498
+
499
+ **Exit codes: 0 success, 1 failure, 2 bad usage, 3 no screen**
500
+
501
+ *A shell can tell 'it worked' from 'it gave up' without parsing any text.*
502
+
503
+ <img src="gallery/cli-exit-codes.png" width="860">
504
+
505
+ `shows:` `main`
506
+
507
+ **The two deliberate acts before the mouse can move**
508
+
509
+ *--real-input without the env var, and --brain vlm with no pixels: both refuse.*
510
+
511
+ <img src="gallery/cli-refusals.png" width="860">
512
+
513
+ `shows:` `main`, `ALLOW_REAL_INPUT_ENV`
514
+
515
+ **--live with no display is refused, with the fix**
516
+
517
+ *It tells you what to do instead of dying inside an episode.*
518
+
519
+ <img src="gallery/cli-live-headless.png" width="860">
520
+
521
+ `shows:` `display_available`, `main`
522
+
523
+ **--live (your real screen, dry run)**
524
+
525
+ *Real capture, real OCR, a real element index - and not one click.*
526
+
527
+ <img src="gallery/cli-live-dry-run.png" width="860">
528
+
529
+ `shows:` `GrabScreen`, `IndexedScreen`, `AutoDetector`, `main`
530
+
531
+ ### Real desktop
532
+
533
+ **GrabScreen: capture a real desktop**
534
+
535
+ *1280x800 of a real X server. mss first, Pillow's ImageGrab as the fallback.*
536
+
537
+ <img src="gallery/desk-capture.png" width="860">
538
+
539
+ `shows:` `GrabScreen`, `GrabScreen.capture`, `ScreenUnavailable`
540
+
541
+ **The element index, drawn on the pixels it came from**
542
+
543
+ *Every box is a real OCR word with a real screen coordinate: the model picks a label, not a pixel.*
544
+
545
+ <img src="gallery/desk-ocr-index.png" width="860">
546
+
547
+ `shows:` `OcrDetector`, `OcrDetector.detect`, `IndexedScreen`, `Element.center`
548
+
549
+ **A real click, and the window's own reaction**
550
+
551
+ *Pixels in, a label chosen, a pixel resolved, a button pressed - and the dialog closed itself.*
552
+
553
+ <img src="gallery/desk-click.png" width="860">
554
+
555
+ `shows:` `PyAutoGuiExecutor`, `AgentLoop.run`, `Policy.dry_run`, `resolve_point`
556
+
557
+ **Success only when the harness can see it**
558
+
559
+ *The app changed its own title and wrote a marker file; the verifier saw the change.*
560
+
561
+ <img src="gallery/desk-verify.png" width="860">
562
+
563
+ `shows:` `AgentLoop.run`, `Episode.success`, `Episode.stop_reason`
564
+
565
+ **A screen with no text still has legal moves**
566
+
567
+ *OCR finds nothing on a canvas, so the index degrades to numbered cells - and says so.*
568
+
569
+ <img src="gallery/desk-grid-fallback.png" width="860">
570
+
571
+ `shows:` `AutoDetector`, `GridDetector`, `OcrDetector`
572
+
573
+ **The safety gate, on a real button, in a real window**
574
+
575
+ *The Terminal button is visible and labelled - and the policy refuses to press it.*
576
+
577
+ <img src="gallery/desk-policy-refusal.png" width="860">
578
+
579
+ `shows:` `Policy.check`, `Policy.deny_labels`, `PolicyViolation`, `Observation.find`
580
+
581
+ **The same click, driven end to end from the CLI**
582
+
583
+ *--live --real-input --verify-text Saved: capture, OCR, click, verify, exit code.*
584
+
585
+ <img src="gallery/desk-cli-real-input.png" width="860">
586
+
587
+ `shows:` `main`, `PyAutoGuiExecutor`, `AgentLoop.run`
588
+
589
+ ### Extensibility
590
+
591
+ **Every stage is a Protocol: bring your own screen, brain, detector, executor**
592
+
593
+ *The loop depends on four one-method contracts, not on the shipped implementations.*
594
+
595
+ <img src="gallery/protocols.png" width="860">
596
+
597
+ `shows:` `Brain`, `Brain.decide`, `ScreenSource`, `ScreenSource.capture`, `Detector`, `Detector.detect`, `Executor`, `Executor.execute`, `ActionResult`
gallery/auto-detector.png ADDED
gallery/benchmark-traces.png ADDED
gallery/benchmark.png ADDED
gallery/brain-mock.png ADDED
gallery/brain-planner.png ADDED
gallery/build-agent.png ADDED
gallery/capture-options.png ADDED
gallery/cli-detector.png ADDED
gallery/cli-doctor.png ADDED
gallery/cli-exit-codes.png ADDED
gallery/cli-image.png ADDED
gallery/cli-live-dry-run.png ADDED
gallery/cli-live-headless.png ADDED
gallery/cli-refusals.png ADDED
gallery/cli-script.png ADDED
gallery/cli-trace.png ADDED
gallery/cli-verify-text.png ADDED
gallery/decision-echo-guard.png ADDED
gallery/decision-garbled.png ADDED
gallery/decision-json.png ADDED
gallery/desk-capture.png ADDED
gallery/desk-cli-real-input.png ADDED
gallery/desk-click.png ADDED
gallery/desk-grid-fallback.png ADDED
gallery/desk-ocr-index.png ADDED
gallery/desk-policy-refusal.png ADDED
gallery/desk-verify.png ADDED
gallery/detector-available.png ADDED
gallery/display-available.png ADDED
gallery/element-matching.png ADDED
gallery/exec-dry-run.png ADDED
gallery/exec-refuses-unresolved.png ADDED
gallery/fake-desktop.png ADDED
gallery/grid-detector.png ADDED
gallery/index.json ADDED
@@ -0,0 +1,864 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "vocab-verbs",
4
+ "category": "Action vocabulary",
5
+ "title": "The six verbs, and only six",
6
+ "shows": [
7
+ "ACTION_KINDS",
8
+ "ACTION_MENU",
9
+ "Action",
10
+ "Action.describe",
11
+ "Action.to_dict",
12
+ "parse_action"
13
+ ],
14
+ "note": "Every action is validated on construction, before anything touches the machine.",
15
+ "kind": "terminal"
16
+ },
17
+ {
18
+ "id": "vocab-refusals",
19
+ "category": "Action vocabulary",
20
+ "title": "Malformed actions are refused, not guessed at",
21
+ "shows": [
22
+ "ActionError",
23
+ "Action",
24
+ "parse_action"
25
+ ],
26
+ "note": "An unknown verb or a missing target raises before the machine is touched.",
27
+ "kind": "terminal"
28
+ },
29
+ {
30
+ "id": "vocab-menu",
31
+ "category": "Action vocabulary",
32
+ "title": "The contract handed to a model",
33
+ "shows": [
34
+ "ACTION_MENU"
35
+ ],
36
+ "note": "The prompt-side menu: one JSON object, six shapes, no invented verbs.",
37
+ "kind": "terminal"
38
+ },
39
+ {
40
+ "id": "menu-numbered",
41
+ "category": "Decisions",
42
+ "title": "A short numbered menu, in reading order",
43
+ "shows": [
44
+ "DecisionMenu",
45
+ "DecisionMenu.render",
46
+ "MenuItem",
47
+ "MenuItem.label"
48
+ ],
49
+ "note": "A 256M model cannot emit coordinates; it can pick a number from twelve.",
50
+ "kind": "terminal"
51
+ },
52
+ {
53
+ "id": "menu-lookup",
54
+ "category": "Decisions",
55
+ "title": "Menu lookup tolerates what models actually send",
56
+ "shows": [
57
+ "DecisionMenu.item",
58
+ "DecisionMenu.find",
59
+ "DecisionMenu.mentioned_in"
60
+ ],
61
+ "note": "1, \"2\", 2.0 and \"Save.\" all resolve; a stray word resolves to nothing.",
62
+ "kind": "terminal"
63
+ },
64
+ {
65
+ "id": "decision-json",
66
+ "category": "Decisions",
67
+ "title": "Reading a decision out of a small model's reply",
68
+ "shows": [
69
+ "parse_decision",
70
+ "extract_json_object",
71
+ "extract_json_objects",
72
+ "DecisionMenu"
73
+ ],
74
+ "note": "The LAST JSON object wins, because a model that echoes its prompt answers last.",
75
+ "kind": "terminal"
76
+ },
77
+ {
78
+ "id": "decision-echo-guard",
79
+ "category": "Decisions",
80
+ "title": "An echoed prompt is refused; a right answer in the wrong field is recovered",
81
+ "shows": [
82
+ "parse_decision",
83
+ "BrainError",
84
+ "DecisionMenu.mentioned_in"
85
+ ],
86
+ "note": "Real 256M/0.5B failure modes, both caught: no blind click either way.",
87
+ "kind": "terminal"
88
+ },
89
+ {
90
+ "id": "decision-garbled",
91
+ "category": "Decisions",
92
+ "title": "A garbled verb with a valid label is still usable",
93
+ "shows": [
94
+ "parse_decision",
95
+ "BrainError"
96
+ ],
97
+ "note": "A 256M model really does emit {\"kind\": \"file\", \"label\": \"Save\"}.",
98
+ "kind": "terminal"
99
+ },
100
+ {
101
+ "id": "exec-dry-run",
102
+ "category": "Execution",
103
+ "title": "Dry run resolves every click to pixels - and touches nothing",
104
+ "shows": [
105
+ "DryRunExecutor",
106
+ "DryRunExecutor.execute",
107
+ "resolve_point",
108
+ "ActionResult"
109
+ ],
110
+ "note": "The default everywhere: read a whole episode before anything clicks.",
111
+ "kind": "terminal"
112
+ },
113
+ {
114
+ "id": "exec-refuses-unresolved",
115
+ "category": "Execution",
116
+ "title": "Real input refuses to guess a pixel for a label it cannot find",
117
+ "shows": [
118
+ "ActionResult.summary",
119
+ "InputUnavailable",
120
+ "PyAutoGuiExecutor",
121
+ "PyAutoGuiExecutor.execute"
122
+ ],
123
+ "note": "This is the gate between a hallucinated label and 'Delete account'.",
124
+ "kind": "terminal"
125
+ },
126
+ {
127
+ "id": "loop-end-to-end",
128
+ "category": "The loop",
129
+ "title": "observe -> think -> act -> verify, traced end to end",
130
+ "shows": [
131
+ "AgentLoop",
132
+ "AgentLoop.run",
133
+ "Episode",
134
+ "Episode.render",
135
+ "Episode.to_rows",
136
+ "Step",
137
+ "Step.line",
138
+ "Step.ok"
139
+ ],
140
+ "note": "One call runs a whole episode; every step is kept for audit.",
141
+ "kind": "terminal"
142
+ },
143
+ {
144
+ "id": "loop-budget",
145
+ "category": "The loop",
146
+ "title": "A hard step budget, so a run cannot wander",
147
+ "shows": [
148
+ "Policy",
149
+ "Policy.step_budget",
150
+ "AgentLoop.run"
151
+ ],
152
+ "note": "The budget ends the episode and says so; nothing executes past it.",
153
+ "kind": "terminal"
154
+ },
155
+ {
156
+ "id": "loop-no-progress-guard",
157
+ "category": "The loop",
158
+ "title": "Repeating one action against an unchanged screen is stopped",
159
+ "shows": [
160
+ "AgentLoop.run",
161
+ "Step",
162
+ "Step.error"
163
+ ],
164
+ "note": "A stuck agent is not a persistent one - it is stopped and told why.",
165
+ "kind": "terminal"
166
+ },
167
+ {
168
+ "id": "loop-kill-switch",
169
+ "category": "The loop",
170
+ "title": "A kill-switch file aborts before the next action",
171
+ "shows": [
172
+ "Policy.kill_switch_path",
173
+ "Policy.should_stop",
174
+ "AgentLoop.run"
175
+ ],
176
+ "note": "Checked before every single action: create the file, the run stops.",
177
+ "kind": "terminal"
178
+ },
179
+ {
180
+ "id": "loop-survives-failures",
181
+ "category": "The loop",
182
+ "title": "A dead screen or a broken brain is an episode, not a traceback",
183
+ "shows": [
184
+ "AgentLoop.run",
185
+ "Episode.stop_reason",
186
+ "GoalUnreachable",
187
+ "BrainError"
188
+ ],
189
+ "note": "Every failure path is recorded as a failed step with a reason.",
190
+ "kind": "terminal"
191
+ },
192
+ {
193
+ "id": "loop-verifier",
194
+ "category": "The loop",
195
+ "title": "Success is evidence, not the model's word",
196
+ "shows": [
197
+ "AgentLoop.run",
198
+ "Episode.success",
199
+ "Observation.text"
200
+ ],
201
+ "note": "Without a verifier, a successful action that removes its own target looks like a failure.",
202
+ "kind": "terminal"
203
+ },
204
+ {
205
+ "id": "loop-on-step",
206
+ "category": "The loop",
207
+ "title": "Every step can be streamed out as it happens",
208
+ "shows": [
209
+ "AgentLoop.run",
210
+ "Step.line",
211
+ "on_step"
212
+ ],
213
+ "note": "This is the hook a live console, a progress bar or a websocket uses.",
214
+ "kind": "terminal"
215
+ },
216
+ {
217
+ "id": "safety-deny-text",
218
+ "category": "Safety",
219
+ "title": "Typing from the deny list never reaches the keyboard",
220
+ "shows": [
221
+ "Policy",
222
+ "Policy.check",
223
+ "PolicyViolation",
224
+ "DEFAULT_DENY_TEXT"
225
+ ],
226
+ "note": "Checked before execution; a refusal ends the episode and clicks nothing.",
227
+ "kind": "terminal"
228
+ },
229
+ {
230
+ "id": "safety-deny-label",
231
+ "category": "Safety",
232
+ "title": "Clicking a denied widget is refused, however the label is spelled",
233
+ "shows": [
234
+ "Policy.deny_labels",
235
+ "Policy.check",
236
+ "Observation.find"
237
+ ],
238
+ "note": "Terminal, keychain, wallet, password manager, bank - matched on the label.",
239
+ "kind": "terminal"
240
+ },
241
+ {
242
+ "id": "safety-deny-keys",
243
+ "category": "Safety",
244
+ "title": "Key chords on the deny list are never pressed",
245
+ "shows": [
246
+ "Policy.deny_keys",
247
+ "Policy.check",
248
+ "PolicyViolation"
249
+ ],
250
+ "note": "ctrl+alt+t, ctrl+alt+delete and ctrl+shift+escape are refused by default.",
251
+ "kind": "terminal"
252
+ },
253
+ {
254
+ "id": "safety-blind-grid",
255
+ "category": "Safety",
256
+ "title": "A numbered grid cell is a blind click: allowed to plan, refused for real",
257
+ "shows": [
258
+ "Policy.allow_grid_clicks",
259
+ "Policy.check",
260
+ "GridDetector"
261
+ ],
262
+ "note": "With real input the blind guess is refused unless you pass --allow-grid.",
263
+ "kind": "terminal"
264
+ },
265
+ {
266
+ "id": "screen-fake",
267
+ "category": "Screens",
268
+ "title": "A scripted screen, for testing the loop with no display",
269
+ "shows": [
270
+ "FakeScreen",
271
+ "FakeScreen.capture",
272
+ "Observation"
273
+ ],
274
+ "note": "The final frame repeats forever, so a runaway agent cannot exhaust the script.",
275
+ "kind": "terminal"
276
+ },
277
+ {
278
+ "id": "screen-image",
279
+ "category": "Screens",
280
+ "title": "A still image (or a sequence) replayed as a screen",
281
+ "shows": [
282
+ "ImageScreen",
283
+ "ImageScreen.capture",
284
+ "fake_desktop"
285
+ ],
286
+ "note": "Lets the whole pipeline be exercised with no display, and a failure be replayed.",
287
+ "kind": "terminal"
288
+ },
289
+ {
290
+ "id": "capture-options",
291
+ "category": "Screens",
292
+ "title": "Real capture: monitors, downscaling, and HiDPI mapping",
293
+ "shows": [
294
+ "GrabScreen",
295
+ "GrabScreen.capture",
296
+ "GrabScreen.map_to_screen",
297
+ "ScreenUnavailable"
298
+ ],
299
+ "note": "Frame pixel -> screen pixel is preserved, so a click cannot drift.",
300
+ "kind": "terminal"
301
+ },
302
+ {
303
+ "id": "element-matching",
304
+ "category": "Screens",
305
+ "title": "Labels are compared on their alphanumeric core",
306
+ "shows": [
307
+ "Element",
308
+ "Element.center",
309
+ "Element.matches",
310
+ "Observation.find",
311
+ "normalise_label"
312
+ ],
313
+ "note": "OCR loses spaces, models add punctuation - so nothing matches by exact string.",
314
+ "kind": "terminal"
315
+ },
316
+ {
317
+ "id": "observation-summary",
318
+ "category": "Screens",
319
+ "title": "The compact text a model is shown",
320
+ "shows": [
321
+ "Observation.summary"
322
+ ],
323
+ "note": "This is the whole world as the brain sees it - capped, and honest when empty.",
324
+ "kind": "terminal"
325
+ },
326
+ {
327
+ "id": "detector-available",
328
+ "category": "Perception",
329
+ "title": "Probing for a detector, and building one by name",
330
+ "shows": [
331
+ "detector_available",
332
+ "make_detector",
333
+ "DetectorUnavailable",
334
+ "Detector"
335
+ ],
336
+ "note": "The doctor and the CLI both ask this before promising anything.",
337
+ "kind": "terminal"
338
+ },
339
+ {
340
+ "id": "ocr-labels",
341
+ "category": "Perception",
342
+ "title": "OCR turns a screenshot into labelled, clickable elements",
343
+ "shows": [
344
+ "OcrDetector",
345
+ "OcrDetector.detect",
346
+ "Element"
347
+ ],
348
+ "note": "The model never has to guess where anything is: this is the grounding.",
349
+ "kind": "terminal"
350
+ },
351
+ {
352
+ "id": "ocr-filters",
353
+ "category": "Perception",
354
+ "title": "Three knobs keep dialog prose out of the menu",
355
+ "shows": [
356
+ "OcrDetector.min_score",
357
+ "OcrDetector.max_label_chars",
358
+ "OcrDetector.min_label_chars"
359
+ ],
360
+ "note": "Every extra line of noise is one more chance for a small model to click prose.",
361
+ "kind": "terminal"
362
+ },
363
+ {
364
+ "id": "grid-detector",
365
+ "category": "Perception",
366
+ "title": "A numbered grid, for screens with no text at all",
367
+ "shows": [
368
+ "GridDetector",
369
+ "GridDetector.detect"
370
+ ],
371
+ "note": "An agent with an empty index has no legal moves; a grid always has moves.",
372
+ "kind": "terminal"
373
+ },
374
+ {
375
+ "id": "auto-detector",
376
+ "category": "Perception",
377
+ "title": "OCR when there is text, a grid when there is none",
378
+ "shows": [
379
+ "AutoDetector",
380
+ "AutoDetector.detect"
381
+ ],
382
+ "note": "The default: useful with nothing installed, better the moment OCR appears.",
383
+ "kind": "terminal"
384
+ },
385
+ {
386
+ "id": "indexed-screen",
387
+ "category": "Perception",
388
+ "title": "IndexedScreen: capture + detect, composed",
389
+ "shows": [
390
+ "IndexedScreen",
391
+ "IndexedScreen.capture",
392
+ "Observation.summary"
393
+ ],
394
+ "note": "One capture() call gives the loop an index it can act on.",
395
+ "kind": "terminal"
396
+ },
397
+ {
398
+ "id": "display-available",
399
+ "category": "Preflight",
400
+ "title": "Is there a desktop session to drive at all?",
401
+ "shows": [
402
+ "display_available"
403
+ ],
404
+ "note": "The first question the CLI and the doctor both ask.",
405
+ "kind": "terminal"
406
+ },
407
+ {
408
+ "id": "run-checks",
409
+ "category": "Preflight",
410
+ "title": "Every precondition, one list, each with the fix",
411
+ "shows": [
412
+ "run_checks",
413
+ "Check",
414
+ "Check.render"
415
+ ],
416
+ "note": "Eight checks; a required one that fails is a blocking problem.",
417
+ "kind": "terminal"
418
+ },
419
+ {
420
+ "id": "cli-doctor",
421
+ "category": "Preflight",
422
+ "title": "python -m tiny_computer_agent --doctor",
423
+ "shows": [
424
+ "detector_available",
425
+ "display_available",
426
+ "doctor",
427
+ "report"
428
+ ],
429
+ "note": "The first thing to run on a new machine. Exits non-zero when not ready.",
430
+ "kind": "terminal"
431
+ },
432
+ {
433
+ "id": "build-agent",
434
+ "category": "Preflight",
435
+ "title": "One call wires screen + detector + brain + executor + policy",
436
+ "shows": [
437
+ "build_agent",
438
+ "Policy",
439
+ "AgentLoop"
440
+ ],
441
+ "note": "Defaults are the safe ones: dry run, a step budget, and a kill-switch file.",
442
+ "kind": "terminal"
443
+ },
444
+ {
445
+ "id": "models-deps",
446
+ "category": "Models",
447
+ "title": "Which model runtime is present, and are the weights cached?",
448
+ "shows": [
449
+ "model_dependencies",
450
+ "model_cached",
451
+ "MODEL_BRAINS",
452
+ "make_model_brain",
453
+ "DEFAULT_TEXT_MODEL",
454
+ "DEFAULT_VISION_MODEL",
455
+ "BrainError"
456
+ ],
457
+ "note": "Weights are loaded lazily, so asking is free.",
458
+ "kind": "terminal"
459
+ },
460
+ {
461
+ "id": "models-device",
462
+ "category": "Models",
463
+ "title": "Device and dtype selection",
464
+ "shows": [
465
+ "resolve_device",
466
+ "pick_dtype"
467
+ ],
468
+ "note": "CUDA if it is there, CPU otherwise; the harness never needs a GPU.",
469
+ "kind": "terminal"
470
+ },
471
+ {
472
+ "id": "models-prompt",
473
+ "category": "Models",
474
+ "title": "The prompt a model actually receives - and how it learns from a dead end",
475
+ "shows": [
476
+ "ModelBrain",
477
+ "ModelBrain.build_prompt",
478
+ "ModelBrain.decide"
479
+ ],
480
+ "note": "Placeholders, not worked examples: a weak model echoes the prompt, and an echo fails validation.",
481
+ "kind": "terminal"
482
+ },
483
+ {
484
+ "id": "models-prompt-history",
485
+ "category": "Models",
486
+ "title": "A second decision is told what already failed",
487
+ "shows": [
488
+ "ModelBrain",
489
+ "ModelBrain.build_prompt",
490
+ "ModelBrain.history"
491
+ ],
492
+ "note": "'you already tried this and the screen did NOT change' is the most useful line a small model gets.",
493
+ "kind": "terminal"
494
+ },
495
+ {
496
+ "id": "scenario",
497
+ "category": "Traces & scoring",
498
+ "title": "A task with one right answer, and the frames to show",
499
+ "shows": [
500
+ "Scenario",
501
+ "Scenario.label",
502
+ "Scenario.screen",
503
+ "Trace.render",
504
+ "run_scenario"
505
+ ],
506
+ "note": "Ground truth lives with the task, so a run can be scored at all.",
507
+ "kind": "terminal"
508
+ },
509
+ {
510
+ "id": "trace-record",
511
+ "category": "Traces & scoring",
512
+ "title": "An episode becomes a portable, pixel-free trace",
513
+ "shows": [
514
+ "Trace",
515
+ "Trace.blocked",
516
+ "Trace.choice",
517
+ "Trace.decided",
518
+ "Trace.decision_step",
519
+ "Trace.frames",
520
+ "Trace.load",
521
+ "Trace.name",
522
+ "Trace.record",
523
+ "Trace.save",
524
+ "Trace.to_dict",
525
+ "Trace.to_json",
526
+ "TraceStep",
527
+ "TraceStep.observation",
528
+ "TraceStep.to_dict"
529
+ ],
530
+ "note": "JSON, so it diffs in review and replays on a machine with nothing installed.",
531
+ "kind": "terminal"
532
+ },
533
+ {
534
+ "id": "trace-replay",
535
+ "category": "Traces & scoring",
536
+ "title": "Replay a recorded trace against a different brain",
537
+ "shows": [
538
+ "Trace.replay",
539
+ "Trace.observations",
540
+ "score_trace"
541
+ ],
542
+ "note": "The frames are the fixture; the brain is the only variable. Nothing is captured or clicked.",
543
+ "kind": "terminal"
544
+ },
545
+ {
546
+ "id": "score-trace",
547
+ "category": "Traces & scoring",
548
+ "title": "Counting how often the choice was right",
549
+ "shows": [
550
+ "score_trace",
551
+ "score",
552
+ "score_replay",
553
+ "Verdict",
554
+ "Verdict.line"
555
+ ],
556
+ "note": "Scored on normalised text, so OCR spacing is forgiven - the same bargain the index makes.",
557
+ "kind": "terminal"
558
+ },
559
+ {
560
+ "id": "scoreboard",
561
+ "category": "Traces & scoring",
562
+ "title": "Verdicts collected into an accuracy",
563
+ "shows": [
564
+ "Scoreboard",
565
+ "Scoreboard.accuracy",
566
+ "Scoreboard.accuracy_for",
567
+ "Scoreboard.add",
568
+ "Scoreboard.blocked",
569
+ "Scoreboard.brains",
570
+ "Scoreboard.by_brain",
571
+ "Scoreboard.confusion",
572
+ "Scoreboard.earned",
573
+ "Scoreboard.render",
574
+ "Scoreboard.to_rows",
575
+ "Scoreboard.total"
576
+ ],
577
+ "note": "The number the whole module exists for: which brain is right, and how often.",
578
+ "kind": "terminal"
579
+ },
580
+ {
581
+ "id": "benchmark",
582
+ "category": "Traces & scoring",
583
+ "title": "Every scenario through every brain, in one table",
584
+ "shows": [
585
+ "benchmark",
586
+ "Scoreboard.to_rows"
587
+ ],
588
+ "note": "A fresh brain per task, so state like 'already clicked this' cannot leak between them.",
589
+ "kind": "terminal"
590
+ },
591
+ {
592
+ "id": "benchmark-traces",
593
+ "category": "Traces & scoring",
594
+ "title": "Replay a whole recorded suite against one brain",
595
+ "shows": [
596
+ "benchmark_traces",
597
+ "load_traces",
598
+ "Trace.save"
599
+ ],
600
+ "note": "The literal roadmap step: the frames are fixed, nothing new is captured.",
601
+ "kind": "terminal"
602
+ },
603
+ {
604
+ "id": "cli-image",
605
+ "category": "Command line",
606
+ "title": "--image example (replay a screenshot, dry run)",
607
+ "shows": [
608
+ "main",
609
+ "build_parser",
610
+ "ImageScreen",
611
+ "detector_available"
612
+ ],
613
+ "note": "No display needed: the whole pipeline over a still image.",
614
+ "kind": "terminal"
615
+ },
616
+ {
617
+ "id": "cli-script",
618
+ "category": "Command line",
619
+ "title": "--script (replay a fixed action list instead of a brain)",
620
+ "shows": [
621
+ "MockBrain",
622
+ "demo_screen",
623
+ "parse_action"
624
+ ],
625
+ "note": "The script is validated exactly like a model's answer: nothing skips the gates.",
626
+ "kind": "terminal"
627
+ },
628
+ {
629
+ "id": "cli-verify-text",
630
+ "category": "Command line",
631
+ "title": "--verify-text (the harness decides success, not the brain)",
632
+ "shows": [
633
+ "AgentLoop.run",
634
+ "Episode.success"
635
+ ],
636
+ "note": "Without it, an action that removes its own target looks like a failure.",
637
+ "kind": "terminal"
638
+ },
639
+ {
640
+ "id": "cli-trace",
641
+ "category": "Command line",
642
+ "title": "--trace / --expected (record the run, then score it)",
643
+ "shows": [
644
+ "Trace.record",
645
+ "Trace.save",
646
+ "score_trace"
647
+ ],
648
+ "note": "Writes JSON next to the code, and prints the score straight away.",
649
+ "kind": "terminal"
650
+ },
651
+ {
652
+ "id": "cli-detector",
653
+ "category": "Command line",
654
+ "title": "--no-ocr (index a numbered grid instead)",
655
+ "shows": [
656
+ "GridDetector",
657
+ "make_detector",
658
+ "detector_available"
659
+ ],
660
+ "note": "Useful on a machine with no OCR engine, or for a canvas app.",
661
+ "kind": "terminal"
662
+ },
663
+ {
664
+ "id": "cli-exit-codes",
665
+ "category": "Command line",
666
+ "title": "Exit codes: 0 success, 1 failure, 2 bad usage, 3 no screen",
667
+ "shows": [
668
+ "main"
669
+ ],
670
+ "note": "A shell can tell 'it worked' from 'it gave up' without parsing any text.",
671
+ "kind": "terminal"
672
+ },
673
+ {
674
+ "id": "cli-refusals",
675
+ "category": "Command line",
676
+ "title": "The two deliberate acts before the mouse can move",
677
+ "shows": [
678
+ "main",
679
+ "ALLOW_REAL_INPUT_ENV"
680
+ ],
681
+ "note": "--real-input without the env var, and --brain vlm with no pixels: both refuse.",
682
+ "kind": "terminal"
683
+ },
684
+ {
685
+ "id": "cli-live-headless",
686
+ "category": "Command line",
687
+ "title": "--live with no display is refused, with the fix",
688
+ "shows": [
689
+ "display_available",
690
+ "main"
691
+ ],
692
+ "note": "It tells you what to do instead of dying inside an episode.",
693
+ "kind": "terminal"
694
+ },
695
+ {
696
+ "id": "cli-live-dry-run",
697
+ "category": "Command line",
698
+ "title": "--live (your real screen, dry run)",
699
+ "shows": [
700
+ "GrabScreen",
701
+ "IndexedScreen",
702
+ "AutoDetector",
703
+ "main"
704
+ ],
705
+ "note": "Real capture, real OCR, a real element index - and not one click.",
706
+ "kind": "terminal"
707
+ },
708
+ {
709
+ "id": "desk-capture",
710
+ "category": "Real desktop",
711
+ "title": "GrabScreen: capture a real desktop",
712
+ "shows": [
713
+ "GrabScreen",
714
+ "GrabScreen.capture",
715
+ "ScreenUnavailable"
716
+ ],
717
+ "note": "1280x800 of a real X server. mss first, Pillow's ImageGrab as the fallback.",
718
+ "kind": "desktop"
719
+ },
720
+ {
721
+ "id": "desk-ocr-index",
722
+ "category": "Real desktop",
723
+ "title": "The element index, drawn on the pixels it came from",
724
+ "shows": [
725
+ "OcrDetector",
726
+ "OcrDetector.detect",
727
+ "IndexedScreen",
728
+ "Element.center"
729
+ ],
730
+ "note": "Every box is a real OCR word with a real screen coordinate: the model picks a label, not a pixel.",
731
+ "kind": "desktop"
732
+ },
733
+ {
734
+ "id": "desk-click",
735
+ "category": "Real desktop",
736
+ "title": "A real click, and the window's own reaction",
737
+ "shows": [
738
+ "PyAutoGuiExecutor",
739
+ "AgentLoop.run",
740
+ "Policy.dry_run",
741
+ "resolve_point"
742
+ ],
743
+ "note": "Pixels in, a label chosen, a pixel resolved, a button pressed - and the dialog closed itself.",
744
+ "kind": "desktop"
745
+ },
746
+ {
747
+ "id": "desk-verify",
748
+ "category": "Real desktop",
749
+ "title": "Success only when the harness can see it",
750
+ "shows": [
751
+ "AgentLoop.run",
752
+ "Episode.success",
753
+ "Episode.stop_reason"
754
+ ],
755
+ "note": "The app changed its own title and wrote a marker file; the verifier saw the change.",
756
+ "kind": "desktop"
757
+ },
758
+ {
759
+ "id": "desk-grid-fallback",
760
+ "category": "Real desktop",
761
+ "title": "A screen with no text still has legal moves",
762
+ "shows": [
763
+ "AutoDetector",
764
+ "GridDetector",
765
+ "OcrDetector"
766
+ ],
767
+ "note": "OCR finds nothing on a canvas, so the index degrades to numbered cells - and says so.",
768
+ "kind": "desktop"
769
+ },
770
+ {
771
+ "id": "desk-policy-refusal",
772
+ "category": "Real desktop",
773
+ "title": "The safety gate, on a real button, in a real window",
774
+ "shows": [
775
+ "Policy.check",
776
+ "Policy.deny_labels",
777
+ "PolicyViolation",
778
+ "Observation.find"
779
+ ],
780
+ "note": "The Terminal button is visible and labelled - and the policy refuses to press it.",
781
+ "kind": "desktop"
782
+ },
783
+ {
784
+ "id": "desk-cli-real-input",
785
+ "category": "Real desktop",
786
+ "title": "The same click, driven end to end from the CLI",
787
+ "shows": [
788
+ "main",
789
+ "PyAutoGuiExecutor",
790
+ "AgentLoop.run"
791
+ ],
792
+ "note": "--live --real-input --verify-text Saved: capture, OCR, click, verify, exit code.",
793
+ "kind": "desktop"
794
+ },
795
+ {
796
+ "id": "protocols",
797
+ "category": "Extensibility",
798
+ "title": "Every stage is a Protocol: bring your own screen, brain, detector, executor",
799
+ "shows": [
800
+ "Brain",
801
+ "Brain.decide",
802
+ "ScreenSource",
803
+ "ScreenSource.capture",
804
+ "Detector",
805
+ "Detector.detect",
806
+ "Executor",
807
+ "Executor.execute",
808
+ "ActionResult"
809
+ ],
810
+ "note": "The loop depends on four one-method contracts, not on the shipped implementations.",
811
+ "kind": "terminal"
812
+ },
813
+ {
814
+ "id": "brain-mock",
815
+ "category": "Decisions",
816
+ "title": "MockBrain: replay a fixed script, exactly once",
817
+ "shows": [
818
+ "MockBrain",
819
+ "MockBrain.decide",
820
+ "MockBrain.remaining"
821
+ ],
822
+ "note": "Deterministic and offline - the workhorse for tests, demos and replays.",
823
+ "kind": "terminal"
824
+ },
825
+ {
826
+ "id": "brain-planner",
827
+ "category": "Decisions",
828
+ "title": "PlannerBrain: no weights, no download, and it gives up honestly",
829
+ "shows": [
830
+ "PlannerBrain",
831
+ "PlannerBrain.decide",
832
+ "GoalUnreachable"
833
+ ],
834
+ "note": "The proof the harness works end to end, and a real fallback when no model is wanted.",
835
+ "kind": "terminal"
836
+ },
837
+ {
838
+ "id": "models-swap",
839
+ "category": "Models",
840
+ "title": "The two real model backends, and lazy loading",
841
+ "shows": [
842
+ "TextPlannerBrain",
843
+ "VLMBrain",
844
+ "TextPlannerBrain.unload",
845
+ "VLMBrain.unload",
846
+ "ModelBrain.loaded",
847
+ "make_model_brain"
848
+ ],
849
+ "note": "Constructing a brain costs nothing; the weights arrive on the first decide().",
850
+ "kind": "terminal"
851
+ },
852
+ {
853
+ "id": "fake-desktop",
854
+ "category": "Screens",
855
+ "title": "The built-in synthetic desktop",
856
+ "shows": [
857
+ "fake_desktop",
858
+ "write_fake_desktop",
859
+ "OcrDetector"
860
+ ],
861
+ "note": "Draws a screenshot-like desktop offline - the same image --image example replays.",
862
+ "kind": "terminal"
863
+ }
864
+ ]
gallery/indexed-screen.png ADDED
gallery/loop-budget.png ADDED
gallery/loop-end-to-end.png ADDED
gallery/loop-kill-switch.png ADDED
gallery/loop-no-progress-guard.png ADDED
gallery/loop-on-step.png ADDED
gallery/loop-survives-failures.png ADDED
gallery/loop-verifier.png ADDED
gallery/menu-lookup.png ADDED
gallery/menu-numbered.png ADDED
gallery/models-deps.png ADDED
gallery/models-device.png ADDED
gallery/models-prompt-history.png ADDED