hkblockchaintechnology commited on
Commit
1228316
·
verified ·
1 Parent(s): c3f2ea5

Publish showcase_catalog.py from marimo

Browse files
Files changed (1) hide show
  1. showcase_catalog.py +1493 -0
showcase_catalog.py ADDED
@@ -0,0 +1,1493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The capability catalogue: what the harness can do, and the proof of each one.
2
+
3
+ Every entry names the public callables it demonstrates, so the gallery in the model
4
+ card can promise that nothing in the API is left without a picture.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ from showcase_core import (
12
+ CATALOG, PY, ROOT, SCENES, Shot, annotate, close_scene, crop, grab, open_scene,
13
+ overlay_lines, side_by_side,
14
+ )
15
+ import os
16
+ import subprocess
17
+
18
+
19
+ def term(sid, category, title, shows, note="", argv=None, code=None, env=None):
20
+ CATALOG.append(Shot(id=sid, category=category, title=title, shows=shows, note=note,
21
+ kind="terminal", argv=argv, code=code, env=env))
22
+
23
+
24
+ def desk(sid, category, title, shows, note="", scene_name=None, make=None):
25
+ CATALOG.append(Shot(id=sid, category=category, title=title, shows=shows, note=note,
26
+ kind="desktop", scene=scene_name, make=make))
27
+
28
+
29
+ def cli(*flags, goal=None):
30
+ argv = [PY, "-m", "tiny_computer_agent"]
31
+ if goal:
32
+ argv.append(goal)
33
+ return argv + list(flags)
34
+
35
+
36
+ # =========================================================================== #
37
+ # 1. the closed action vocabulary
38
+ # =========================================================================== #
39
+
40
+ term("vocab-verbs", "Action vocabulary",
41
+ "The six verbs, and only six",
42
+ ["Action", "parse_action", "ACTION_KINDS", "ACTION_MENU", "Action.describe"],
43
+ note="Every action is validated on construction, before anything touches the machine.",
44
+ code="""
45
+ from tiny_computer_agent import ACTION_KINDS, parse_action
46
+
47
+ print("the whole vocabulary:", ", ".join(ACTION_KINDS))
48
+ print()
49
+ for payload in [
50
+ {"kind": "click", "label": "Save"},
51
+ {"kind": "click", "x": 640, "y": 400},
52
+ {"kind": "type", "text": "report"},
53
+ {"kind": "key", "keys": "ctrl+s"},
54
+ {"kind": "scroll", "amount": -3},
55
+ {"kind": "wait", "seconds": 1.5},
56
+ {"kind": "done", "reason": "the file is saved"},
57
+ ]:
58
+ action = parse_action(dict(payload, reason="demonstration"))
59
+ print(f" parsed -> {action.describe()}")
60
+ """)
61
+
62
+ term("vocab-refusals", "Action vocabulary",
63
+ "Malformed actions are refused, not guessed at",
64
+ ["ActionError", "Action", "parse_action"],
65
+ note="An unknown verb or a missing target raises before the machine is touched.",
66
+ code="""
67
+ from tiny_computer_agent import ActionError, parse_action
68
+
69
+ print("every one of these is refused, with the reason why:")
70
+ for payload in [
71
+ {"kind": "format_disk"}, # not a verb we have
72
+ {"kind": "click"}, # click with no target
73
+ {"kind": "click", "label": "Save", "x": -5, "y": 2}, # negative pixel
74
+ {"kind": "type"}, # type with no text
75
+ {"kind": "key"}, # key with no chord
76
+ {"kind": "scroll", "amount": 0}, # scroll that scrolls nowhere
77
+ {"kind": "wait", "seconds": 0}, # wait that does not wait
78
+ ]:
79
+ try:
80
+ parse_action(payload)
81
+ print(f" ACCEPTED (bug!): {payload}")
82
+ except ActionError as exc:
83
+ print(f" {str(payload)[:38]:40} -> {exc}")
84
+ """)
85
+
86
+ term("vocab-menu", "Action vocabulary",
87
+ "The contract handed to a model",
88
+ ["ACTION_MENU"],
89
+ note="The prompt-side menu: one JSON object, six shapes, no invented verbs.",
90
+ code="""
91
+ from tiny_computer_agent import ACTION_MENU
92
+ print(ACTION_MENU)
93
+ """)
94
+
95
+ # =========================================================================== #
96
+ # 2. menus and lenient parsing
97
+ # =========================================================================== #
98
+
99
+ term("menu-numbered", "Decisions",
100
+ "A short numbered menu, in reading order",
101
+ ["DecisionMenu", "DecisionMenu.render", "MenuItem", "MenuItem.label"],
102
+ note="A 256M model cannot emit coordinates; it can pick a number from twelve.",
103
+ code="""
104
+ from tiny_computer_agent import DecisionMenu, Element, Observation
105
+
106
+ frame = Observation(elements=(
107
+ Element("Dialog", x=40, y=20, width=90, height=24),
108
+ Element("File name", x=200, y=95, width=120, height=22),
109
+ Element("Cancel", x=400, y=270, width=110, height=30),
110
+ Element("Save", x=200, y=270, width=110, height=30),
111
+ ), text="Untitled document - unsaved changes")
112
+
113
+ menu = DecisionMenu(frame)
114
+ print(f"{len(menu)} numbered choices, sorted top-to-bottom then left-to-right:")
115
+ print(menu.render())
116
+ print()
117
+ print("capped at 3 for a small context window:")
118
+ print(DecisionMenu(frame, max_items=3).render())
119
+ """)
120
+
121
+ term("menu-lookup", "Decisions",
122
+ "Menu lookup tolerates what models actually send",
123
+ ["DecisionMenu.item", "DecisionMenu.find", "DecisionMenu.mentioned_in"],
124
+ note="1, \"2\", 2.0 and \"Save.\" all resolve; a stray word resolves to nothing.",
125
+ code="""
126
+ from tiny_computer_agent import DecisionMenu, Element, Observation
127
+
128
+ frame = Observation(elements=(
129
+ Element("Files", x=10, y=6, width=60, height=20),
130
+ Element("Save .", x=200, y=270, width=90, height=30), # OCR kept the full stop
131
+ Element("Cancel", x=400, y=270, width=90, height=30),
132
+ ))
133
+ menu = DecisionMenu(frame)
134
+
135
+ for probe in [1, "2", 2.0, "2 ", 0, 99, "two"]:
136
+ item = menu.item(probe)
137
+ print(f" item({probe!r:6}) -> {item.label if item else 'no such item'}")
138
+
139
+ print()
140
+ for label in ["save", "SAVE", "Save .", "nothing here"]:
141
+ found = menu.find(label)
142
+ print(f" find({label!r:15}) -> {found.label if found else 'not on screen'}")
143
+
144
+ print()
145
+ print("mentioned_in('click Save or Cancel'):",
146
+ [i.label for i in menu.mentioned_in("click Save or Cancel")])
147
+ """)
148
+
149
+ term("decision-json", "Decisions",
150
+ "Reading a decision out of a small model's reply",
151
+ ["parse_decision", "extract_json_object", "extract_json_objects", "DecisionMenu"],
152
+ note="The LAST JSON object wins, because a model that echoes its prompt answers last.",
153
+ code="""
154
+ from tiny_computer_agent import DecisionMenu, Element, Observation, extract_json_objects, parse_decision
155
+
156
+ frame = Observation(elements=(
157
+ Element("Files", x=10, y=6, width=60, height=20),
158
+ Element("Save", x=200, y=270, width=90, height=30),
159
+ Element("Cancel", x=400, y=270, width=90, height=30),
160
+ ))
161
+
162
+ reply = 'Menu: 1. Files 2. Save 3. Cancel\nAnswer: {"kind": "click", "index": 2}'
163
+ print("reply:", reply.replace(chr(10), " | "), chr(10))
164
+ print("every JSON object found, in order:", extract_json_objects(reply))
165
+ print("the decision taken:", parse_decision(reply, frame).describe())
166
+ print()
167
+ for reply in ["2", "Click on \"Save\" in the dialogue.", "The goal is already done."]:
168
+ print(f" {reply!r:42} -> {parse_decision(reply, frame).describe()}")
169
+ """)
170
+
171
+ term("decision-echo-guard", "Decisions",
172
+ "An echoed prompt is refused; a right answer in the wrong field is recovered",
173
+ ["parse_decision", "BrainError", "DecisionMenu.mentioned_in"],
174
+ note="Real 256M/0.5B failure modes, both caught: no blind click either way.",
175
+ code="""
176
+ from tiny_computer_agent import BrainError, DecisionMenu, Element, Observation, parse_decision
177
+
178
+ frame = Observation(elements=(
179
+ Element("Files", x=10, y=6, width=60, height=20),
180
+ Element("Save", x=200, y=270, width=90, height=30),
181
+ Element("Cancel", x=400, y=270, width=90, height=30),
182
+ ))
183
+
184
+ echoed = DecisionMenu(frame).render() # the model read the menu back to us
185
+ try:
186
+ parse_decision(echoed, frame)
187
+ print("echo ACCEPTED (bug!)")
188
+ except BrainError as exc:
189
+ print("echo refused:", exc)
190
+
191
+ print()
192
+ print("a 0.5B model really did answer like this - correct answer in the wrong field:")
193
+ salvaged = parse_decision('{"kind": "click", "index": 0, "reason": "Save"}', frame)
194
+ print(" recovered ->", salvaged.describe())
195
+
196
+ print()
197
+ try:
198
+ parse_decision('{"kind": "click", "index": 42}', frame)
199
+ except BrainError as exc:
200
+ print("an index outside the menu is refused:", exc)
201
+ try:
202
+ parse_decision('{"kind": "click", "label": "Format disk"}', frame)
203
+ except BrainError as exc:
204
+ print("a label that is not on screen is refused:", exc)
205
+ """)
206
+
207
+ term("decision-garbled", "Decisions",
208
+ "A garbled verb with a valid label is still usable",
209
+ ["parse_decision", "BrainError"],
210
+ note="A 256M model really does emit {\"kind\": \"file\", \"label\": \"Save\"}.",
211
+ code="""
212
+ from tiny_computer_agent import Element, Observation, parse_decision
213
+ frame = Observation(elements=(Element("Save", x=200, y=270, width=90, height=30),))
214
+ print('reply: {"kind": "file", "label": "Save"} <- not a verb we have')
215
+ print(" ->", parse_decision('{"kind": "file", "label": "Save"}', frame).describe())
216
+ """)
217
+
218
+ # =========================================================================== #
219
+ # 3. execution: the only two places that touch the machine
220
+ # =========================================================================== #
221
+
222
+ term("exec-dry-run", "Execution",
223
+ "Dry run resolves every click to pixels - and touches nothing",
224
+ ["DryRunExecutor", "DryRunExecutor.execute", "resolve_point", "ActionResult"],
225
+ note="The default everywhere: read a whole episode before anything clicks.",
226
+ code="""
227
+ from tiny_computer_agent import Action, DryRunExecutor, Element, Observation
228
+
229
+ frame = Observation(elements=(Element("Save", x=200, y=270, width=110, height=30),))
230
+ executor = DryRunExecutor()
231
+
232
+ for action in [Action("click", label="Save"),
233
+ Action("click", label="Nowhere"),
234
+ Action("type", text="report.txt"),
235
+ Action("key", keys="ctrl+s"),
236
+ Action("scroll", amount=-3),
237
+ Action("wait", seconds=0.5),
238
+ Action("done", reason="finished")]:
239
+ result = executor.execute(action, frame)
240
+ print(f" {result.summary()}")
241
+
242
+ print()
243
+ print("actions the executor *performed* on the machine:", executor.performed)
244
+ print("the log it kept instead:")
245
+ for line in executor.log:
246
+ print(" ", line)
247
+ """)
248
+
249
+ term("exec-refuses-unresolved", "Execution",
250
+ "Real input refuses to guess a pixel for a label it cannot find",
251
+ ["PyAutoGuiExecutor", "PyAutoGuiExecutor.execute", "ActionResult.summary"],
252
+ note="This is the gate between a hallucinated label and 'Delete account'.",
253
+ code="""
254
+ from tiny_computer_agent import Action, Element, Observation, PyAutoGuiExecutor
255
+
256
+ frame = Observation(elements=(Element("Save", x=200, y=270, width=110, height=30),))
257
+ executor = PyAutoGuiExecutor()
258
+
259
+ result = executor.execute(Action("click", label="Delete account"), frame)
260
+ print("a label that is not on screen:")
261
+ print(" ", result.summary())
262
+ print(" ok =", result.ok, " - nothing was clicked")
263
+
264
+ print()
265
+ resolved = executor.execute(Action("done", reason="nothing to do"), frame)
266
+ print("a verb that needs no target passes straight through:")
267
+ print(" ", resolved.summary())
268
+ """)
269
+
270
+ # =========================================================================== #
271
+ # 4. the loop
272
+ # =========================================================================== #
273
+
274
+ term("loop-end-to-end", "The loop",
275
+ "observe -> think -> act -> verify, traced end to end",
276
+ ["AgentLoop", "AgentLoop.run", "Episode", "Episode.render", "Episode.to_rows", "Step", "Step.line", "Step.ok"],
277
+ note="One call runs a whole episode; every step is kept for audit.",
278
+ code="""
279
+ from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain
280
+
281
+ screen = FakeScreen([
282
+ (Element("Terminal", x=0, y=0, width=70, height=24), Element("Files", x=80, y=0, width=70, height=24)),
283
+ (Element("Name", x=100, y=120, width=200, height=24), Element("Save", x=320, y=120, width=80, height=24)),
284
+ (Element("Save", x=320, y=120, width=80, height=24),),
285
+ ], text="Untitled document - unsaved changes")
286
+
287
+ episode = AgentLoop(
288
+ screen=screen,
289
+ brain=MockBrain([
290
+ {"kind": "click", "label": "Save"},
291
+ {"kind": "type", "text": "report"},
292
+ {"kind": "done", "reason": "the document is saved"},
293
+ ]),
294
+ ).run("save the report")
295
+
296
+ print(episode.render())
297
+ print()
298
+ if episode.success:
299
+ print("the same episode as rows for a table:")
300
+ for row in episode.to_rows():
301
+ print(" ", row)
302
+ """)
303
+
304
+ term("loop-budget", "The loop",
305
+ "A hard step budget, so a run cannot wander",
306
+ ["Policy", "Policy.step_budget", "AgentLoop.run"],
307
+ note="The budget ends the episode and says so; nothing executes past it.",
308
+ code="""
309
+ from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain, Policy
310
+
311
+ episode = AgentLoop(
312
+ screen=FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)]),
313
+ brain=MockBrain([{"kind": "wait", "seconds": 0.2 + i} for i in range(10)]),
314
+ policy=Policy(step_budget=3),
315
+ ).run("click Save")
316
+
317
+ print(episode.render())
318
+ print()
319
+ print("steps taken:", len(episode.steps), "| success:", episode.success)
320
+ """)
321
+
322
+ term("loop-no-progress-guard", "The loop",
323
+ "Repeating one action against an unchanged screen is stopped",
324
+ ["AgentLoop.run", "Step", "Step.error"],
325
+ note="A stuck agent is not a persistent one - it is stopped and told why.",
326
+ code="""
327
+ from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain, Policy
328
+
329
+ episode = AgentLoop(
330
+ screen=FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)]),
331
+ brain=MockBrain([{"kind": "click", "label": "Save"}] * 10),
332
+ policy=Policy(step_budget=10),
333
+ ).run("click Save")
334
+
335
+ print(episode.render())
336
+ print()
337
+ print("stopped after", len(episode.steps), "steps instead of burning all 10:")
338
+ print(" ", episode.steps[-1].error)
339
+ """)
340
+
341
+ term("loop-kill-switch", "The loop",
342
+ "A kill-switch file aborts before the next action",
343
+ ["Policy.kill_switch_path", "Policy.should_stop", "AgentLoop.run"], env={"TCA_DEMO": "1"},
344
+ note="Checked before every single action: create the file, the run stops.",
345
+ code="""
346
+ import os, tempfile
347
+ from pathlib import Path
348
+ from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain, Policy
349
+
350
+ marker = Path(tempfile.mkdtemp()) / "tiny-computer-agent.STOP"
351
+ marker.write_text("stop")
352
+ print("kill switch file:", marker, "- present:", marker.exists())
353
+
354
+ episode = AgentLoop(
355
+ screen=FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)]),
356
+ brain=MockBrain([{"kind": "click", "label": "Save"}]),
357
+ policy=Policy(kill_switch_path=str(marker)),
358
+ ).run("click Save")
359
+
360
+ print()
361
+ print("steps taken:", episode.steps, "(none)")
362
+ print("stop reason:", episode.stop_reason)
363
+ """)
364
+
365
+ term("loop-survives-failures", "The loop",
366
+ "A dead screen or a broken brain is an episode, not a traceback",
367
+ ["AgentLoop.run", "Episode.stop_reason", "GoalUnreachable", "BrainError"],
368
+ note="Every failure path is recorded as a failed step with a reason.",
369
+ code="""
370
+ from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain, PlannerBrain, Policy, GoalUnreachable
371
+
372
+ class Blind:
373
+ def capture(self):
374
+ raise RuntimeError("X connection failed")
375
+
376
+ class Exploding:
377
+ def decide(self, goal, observation):
378
+ raise RuntimeError("the model died")
379
+
380
+ print("a capture that fails:")
381
+ print(" ", AgentLoop(screen=Blind(), brain=MockBrain([{"kind": "done"}])).run("anything").stop_reason)
382
+ print("a brain that crashes:")
383
+ print(" ", AgentLoop(screen=FakeScreen([(Element("Save"),)]), brain=Exploding()).run("save").stop_reason)
384
+ print("a brain that gives up (a failure, never a success):")
385
+ episode = AgentLoop(screen=FakeScreen([(Element("Files"),)]), brain=PlannerBrain(max_waits=0)).run("click 'Save'")
386
+ print(" success:", episode.success, "|", episode.stop_reason)
387
+ """)
388
+
389
+ term("loop-verifier", "The loop",
390
+ "Success is evidence, not the model's word",
391
+ ["AgentLoop.run", "Episode.success", "Observation.text"],
392
+ note="Without a verifier, a successful action that removes its own target looks like a failure.",
393
+ code="""
394
+ from tiny_computer_agent import AgentLoop, Element, MockBrain, Observation, Policy
395
+
396
+ class ScriptedTextScreen:
397
+ def __init__(self, texts): self.texts, self.n = list(texts), 0
398
+ def capture(self) -> Observation:
399
+ text = self.texts[min(self.n, len(self.texts) - 1)]; self.n += 1
400
+ return Observation(index=self.n - 1, elements=(Element("Save"),), text=text)
401
+
402
+ waits = [{"kind": "wait", "seconds": 0.1}] * 4
403
+ screen = lambda: ScriptedTextScreen(["Unsaved changes", "before", "Saved"])
404
+
405
+ without = AgentLoop(screen=screen(), brain=MockBrain(waits), policy=Policy(step_budget=3)).run("save")
406
+ print("no verifier ->", without.success, "|", without.stop_reason)
407
+
408
+ with_verifier = AgentLoop(
409
+ screen=screen(), brain=MockBrain(waits), policy=Policy(step_budget=4),
410
+ verify=lambda observation: "saved" in observation.text.lower(),
411
+ ).run("save")
412
+ print("with verifier ->", with_verifier.success, "|", with_verifier.stop_reason)
413
+ """)
414
+
415
+ term("loop-on-step", "The loop",
416
+ "Every step can be streamed out as it happens",
417
+ ["AgentLoop.run", "Step.line", "on_step"],
418
+ note="This is the hook a live console, a progress bar or a websocket uses.",
419
+ code="""
420
+ from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain
421
+
422
+ seen = []
423
+ episode = AgentLoop(
424
+ screen=FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)]),
425
+ brain=MockBrain([{"kind": "click", "label": "Save"}, {"kind": "done", "reason": "saved"}]),
426
+ on_step=lambda step: seen.append(step.line()),
427
+ ).run("save")
428
+
429
+ print("streamed live, one line per step:")
430
+ for line in seen:
431
+ print(" ", line)
432
+ print()
433
+ print("and the finished episode has", len(episode.steps), "steps")
434
+ """)
435
+
436
+ # =========================================================================== #
437
+ # 5. the safety gates
438
+ # =========================================================================== #
439
+
440
+ term("safety-deny-text", "Safety",
441
+ "Typing from the deny list never reaches the keyboard",
442
+ ["Policy", "Policy.check", "PolicyViolation", "DEFAULT_DENY_TEXT"],
443
+ note="Checked before execution; a refusal ends the episode and clicks nothing.",
444
+ code="""
445
+ from tiny_computer_agent import Action, DryRunExecutor, AgentLoop, Element, FakeScreen, MockBrain, Policy
446
+
447
+ for text in ["report.txt", "sudo rm -rf /", "my api_key is here", "correct horse battery staple"]:
448
+ episode = AgentLoop(
449
+ screen=FakeScreen([(Element("Name", kind="input", x=100, y=120, width=200, height=24),)]),
450
+ brain=MockBrain([{"kind": "type", "text": text}]),
451
+ ).run("fill the form")
452
+ verdict = "typed" if episode.steps[-1].error is None else "REFUSED"
453
+ print(f" type({text!r:32}) -> {verdict}")
454
+ if episode.steps[-1].error:
455
+ print(f" {episode.steps[-1].error}")
456
+ """)
457
+
458
+ term("safety-deny-label", "Safety",
459
+ "Clicking a denied widget is refused, however the label is spelled",
460
+ ["Policy.deny_labels", "Policy.check", "Observation.find"],
461
+ note="Terminal, keychain, wallet, password manager, bank - matched on the label.",
462
+ code="""
463
+ from tiny_computer_agent import Action, Element, Observation, Policy, PolicyViolation
464
+
465
+ frame = Observation(elements=(
466
+ Element("Terminal", x=0, y=0, width=80, height=24),
467
+ Element("Password Manager", x=100, y=0, width=160, height=24),
468
+ Element("Save", x=320, y=120, width=80, height=24),
469
+ ))
470
+ policy = Policy()
471
+
472
+ for label in ["Terminal", "Password Manager", "T E R M I N A L", "Save"]:
473
+ try:
474
+ policy.check(Action("click", label=label), frame)
475
+ print(f" click({label!r:20}) -> allowed")
476
+ except PolicyViolation as exc:
477
+ print(f" click({label!r:20}) -> refused: {exc}")
478
+ """)
479
+
480
+ term("safety-deny-keys", "Safety",
481
+ "Key chords on the deny list are never pressed",
482
+ ["Policy.deny_keys", "Policy.check", "PolicyViolation"],
483
+ note="ctrl+alt+t, ctrl+alt+delete and ctrl+shift+escape are refused by default.",
484
+ code="""
485
+ from tiny_computer_agent import Action, Policy, PolicyViolation
486
+
487
+ policy = Policy()
488
+ for chord in ["ctrl+s", "ctrl+alt+t", "CTRL+SHIFT+ESCAPE", "ctrl+alt+delete"]:
489
+ try:
490
+ policy.check(Action("key", keys=chord))
491
+ print(f" key({chord!r:20}) -> allowed")
492
+ except PolicyViolation as exc:
493
+ print(f" key({chord!r:20}) -> refused: {exc}")
494
+ """)
495
+
496
+ term("safety-blind-grid", "Safety",
497
+ "A numbered grid cell is a blind click: allowed to plan, refused for real",
498
+ ["Policy.allow_grid_clicks", "Policy.check", "GridDetector"],
499
+ note="With real input the blind guess is refused unless you pass --allow-grid.",
500
+ code="""
501
+ from tiny_computer_agent import Action, GridDetector, Policy, PolicyViolation
502
+ from tiny_computer_agent.demo import fake_desktop
503
+
504
+ elements = GridDetector(rows=2, cols=3).detect(fake_desktop())
505
+ from tiny_computer_agent import Observation
506
+ frame = Observation(elements=elements)
507
+ print("a screen with no text degrades to", len(elements), "numbered cells:",
508
+ ", ".join(e.label for e in elements))
509
+ print()
510
+ for policy, name in [(Policy(dry_run=True), "dry run (planning)"),
511
+ (Policy(dry_run=False), "real input, default"),
512
+ (Policy(dry_run=False, allow_grid_clicks=True), "real input, --allow-grid")]:
513
+ try:
514
+ policy.check(Action("click", label=elements[4].label), frame)
515
+ print(f" {name:32} -> allowed")
516
+ except PolicyViolation as exc:
517
+ print(f" {name:32} -> refused: {exc}")
518
+ """)
519
+
520
+ # =========================================================================== #
521
+ # 6. screens: where the pixels come from
522
+ # =========================================================================== #
523
+
524
+ term("screen-fake", "Screens",
525
+ "A scripted screen, for testing the loop with no display",
526
+ ["FakeScreen", "FakeScreen.capture", "Observation"],
527
+ note="The final frame repeats forever, so a runaway agent cannot exhaust the script.",
528
+ code="""
529
+ from tiny_computer_agent import Element, FakeScreen
530
+ screen = FakeScreen([(Element("Files", x=10, y=6, width=60, height=20),),
531
+ (Element("Save", x=320, y=120, width=80, height=24),)], text="Untitled document")
532
+ for i in range(4):
533
+ observation = screen.capture()
534
+ print(f" capture {i} -> index={observation.index} elements={[e.label for e in observation.elements]}")
535
+ """)
536
+
537
+ term("screen-image", "Screens",
538
+ "A still image (or a sequence) replayed as a screen",
539
+ ["ImageScreen", "ImageScreen.capture", "fake_desktop"],
540
+ note="Lets the whole pipeline be exercised with no display, and a failure be replayed.",
541
+ code="""
542
+ from tiny_computer_agent import ImageScreen, fake_desktop
543
+ screen = ImageScreen(fake_desktop())
544
+ for i in range(3):
545
+ observation = screen.capture()
546
+ print(f" capture {i} -> index={observation.index} frame={type(observation.frame).__name__} {observation.frame.size}")
547
+ print()
548
+ print("captures beyond the scripted frames keep returning the last one")
549
+ """)
550
+
551
+ term("capture-options", "Screens",
552
+ "Real capture: monitors, downscaling, and HiDPI mapping",
553
+ ["GrabScreen", "GrabScreen.capture", "GrabScreen.map_to_screen", "ScreenUnavailable"],
554
+ note="Frame pixel -> screen pixel is preserved, so a click cannot drift.",
555
+ code="""
556
+ from tiny_computer_agent import Element, GrabScreen
557
+ for name, screen in [("monitor 1 (native)", GrabScreen(monitor=1)),
558
+ ("max_width=640", GrabScreen(monitor=1, max_width=640)),
559
+ ("scale=2.0 (HiDPI)", GrabScreen(monitor=1, scale=2.0))]:
560
+ observation = screen.capture()
561
+ print(f" {name:20} frame={observation.frame.width}x{observation.frame.height}px "
562
+ f"origin={screen.origin} frame_scale={screen.frame_scale:.3f}")
563
+ print()
564
+ scaled = GrabScreen(monitor=1, max_width=640)
565
+ scaled.capture()
566
+ label = Element("Save", x=100, y=50, width=80, height=24)
567
+ print("a label found at (100,50) in the DOWNSCALED frame maps back to", scaled.map_to_screen(label).center)
568
+ print("on the real screen - which is why the downscale cannot break a click.")
569
+ """)
570
+
571
+ term("element-matching", "Screens",
572
+ "Labels are compared on their alphanumeric core",
573
+ ["Element", "Element.center", "Element.matches", "Observation.find", "normalise_label"],
574
+ note="OCR loses spaces, models add punctuation - so nothing matches by exact string.",
575
+ code="""
576
+ from tiny_computer_agent import Element, Observation, normalise_label
577
+
578
+ save = Element("Save", x=200, y=270, width=110, height=30)
579
+ print("Element('Save', x=200, y=270, width=110, height=30)")
580
+ print(" .center:", save.center, "| .matches('save'):", save.matches("save"))
581
+ print()
582
+ print("normalise_label keeps only letters and digits:")
583
+ for raw in ["Save", "Save .", "S A V E", "save!", "Untitled document - unsaved changes"]:
584
+ print(f" {raw!r:36} -> {normalise_label(raw)!r}")
585
+ print()
586
+ frame = Observation(elements=(Element("Save .", x=10, y=10, width=80, height=24),
587
+ Element("Save As", x=110, y=10, width=80, height=24)))
588
+ for probe in ["Save", "save", "Save As", "Nothing"]:
589
+ hit = frame.find(probe)
590
+ print(f" find({probe!r:10}) -> {hit.label if hit else 'not on screen'!r}")
591
+ """)
592
+
593
+ term("observation-summary", "Screens",
594
+ "The compact text a model is shown",
595
+ ["Observation.summary"],
596
+ note="This is the whole world as the brain sees it - capped, and honest when empty.",
597
+ code="""
598
+ from tiny_computer_agent import Element, Observation
599
+ frame = Observation(index=3, text="Untitled document - unsaved changes", elements=(
600
+ Element("Files", x=10, y=6, width=60, height=20),
601
+ Element("Save", x=200, y=270, width=110, height=30),
602
+ Element("Cancel", x=400, y=270, width=110, height=30),
603
+ ))
604
+ print(frame.summary())
605
+ print()
606
+ print("an empty index says so rather than pretending:")
607
+ print(Observation(index=4).summary())
608
+ """)
609
+
610
+ # =========================================================================== #
611
+ # 7. perception: pixels -> a labelled element index
612
+ # =========================================================================== #
613
+
614
+ term("detector-available", "Perception",
615
+ "Probing for a detector, and building one by name",
616
+ ["detector_available", "make_detector", "DetectorUnavailable", "Detector"],
617
+ note="The doctor and the CLI both ask this before promising anything.",
618
+ code="""
619
+ from tiny_computer_agent import DetectorUnavailable, detector_available, make_detector
620
+ for name in ["auto", "ocr", "grid", "telepathy"]:
621
+ ok, detail = detector_available(name)
622
+ print(f" detector_available({name!r:11}) -> {ok!s:5} {detail}")
623
+ print()
624
+ for name in ["auto", "grid"]:
625
+ print(f" make_detector({name!r:6}) -> {type(make_detector(name)).__name__}")
626
+ try:
627
+ make_detector("telepathy")
628
+ except DetectorUnavailable as exc:
629
+ print(" make_detector('telepathy') -> DetectorUnavailable:", exc)
630
+ """)
631
+
632
+ term("ocr-labels", "Perception",
633
+ "OCR turns a screenshot into labelled, clickable elements",
634
+ ["OcrDetector", "OcrDetector.detect", "Element"],
635
+ note="The model never has to guess where anything is: this is the grounding.",
636
+ code="""
637
+ from tiny_computer_agent import OcrDetector, fake_desktop
638
+ elements = OcrDetector().detect(fake_desktop())
639
+ print(f"OCR read {len(elements)} labelled elements off one screenshot:")
640
+ print()
641
+ for element in elements:
642
+ print(f" {element.label!r:34} kind={element.kind:6} centre={str(element.center):12} box={element.width}x{element.height}")
643
+ """)
644
+
645
+ term("ocr-filters", "Perception",
646
+ "Three knobs keep dialog prose out of the menu",
647
+ ["OcrDetector.min_score", "OcrDetector.max_label_chars", "OcrDetector.min_label_chars"],
648
+ note="Every extra line of noise is one more chance for a small model to click prose.",
649
+ code="""
650
+ from tiny_computer_agent import OcrDetector, fake_desktop
651
+ image = fake_desktop()
652
+ for label, detector in [
653
+ ("default", OcrDetector()),
654
+ ("min_score=0.99", OcrDetector(min_score=0.99)),
655
+ ("max_label_chars=6", OcrDetector(max_label_chars=6)),
656
+ ("min_label_chars=8", OcrDetector(min_label_chars=8)),
657
+ ]:
658
+ print(f" {label:18} -> {[e.label for e in detector.detect(image)]}")
659
+ """)
660
+
661
+ term("grid-detector", "Perception",
662
+ "A numbered grid, for screens with no text at all",
663
+ ["GridDetector", "GridDetector.detect"],
664
+ note="An agent with an empty index has no legal moves; a grid always has moves.",
665
+ code="""
666
+ from tiny_computer_agent import GridDetector, fake_desktop
667
+ for rows, cols in [(2, 3), (8, 12)]:
668
+ cells = GridDetector(rows=rows, cols=cols).detect(fake_desktop())
669
+ print(f" {rows}x{cols} grid -> {len(cells)} cells: "
670
+ f"first={cells[0].label!r} at {cells[0].center}, last={cells[-1].label!r} at {cells[-1].center}")
671
+ """)
672
+
673
+ term("auto-detector", "Perception",
674
+ "OCR when there is text, a grid when there is none",
675
+ ["AutoDetector", "AutoDetector.detect"],
676
+ note="The default: useful with nothing installed, better the moment OCR appears.",
677
+ code="""
678
+ from PIL import Image
679
+ from tiny_computer_agent import AutoDetector, fake_desktop
680
+ detector = AutoDetector()
681
+ with_text = detector.detect(fake_desktop())
682
+ blank = detector.detect(Image.new("RGB", (720, 460), (18, 18, 22)))
683
+ print(" a screenshot with text ->", len(with_text), "elements:", [e.label for e in with_text][:6])
684
+ print(" a screen with no text ->", len(blank), "elements:", [e.label for e in blank][:6])
685
+ """)
686
+
687
+ term("indexed-screen", "Perception",
688
+ "IndexedScreen: capture + detect, composed",
689
+ ["IndexedScreen", "IndexedScreen.capture", "Observation.summary"],
690
+ note="One capture() call gives the loop an index it can act on.",
691
+ code="""
692
+ from tiny_computer_agent import ImageScreen, IndexedScreen, OcrDetector, fake_desktop
693
+ screen = IndexedScreen(ImageScreen(fake_desktop()), OcrDetector())
694
+ observation = screen.capture()
695
+ print(f"capture() composed pixels -> {len(observation.elements)} labelled elements:")
696
+ print()
697
+ print(observation.summary())
698
+ """)
699
+
700
+ # =========================================================================== #
701
+ # 8. preflight: what can this machine do?
702
+ # =========================================================================== #
703
+
704
+ term("display-available", "Preflight",
705
+ "Is there a desktop session to drive at all?",
706
+ ["display_available"],
707
+ note="The first question the CLI and the doctor both ask.",
708
+ code="""
709
+ from tiny_computer_agent import display_available
710
+ ok, detail = display_available()
711
+ print("display_available() ->", ok)
712
+ print(" detail:", detail)
713
+ """)
714
+
715
+ term("run-checks", "Preflight",
716
+ "Every precondition, one list, each with the fix",
717
+ ["run_checks", "Check", "Check.render"],
718
+ note="Eight checks; a required one that fails is a blocking problem.",
719
+ code="""
720
+ from tiny_computer_agent import run_checks
721
+ checks = run_checks(brain="planner", detector="ocr", real_input=False)
722
+ for check in checks:
723
+ print(check.render())
724
+ blocking = [c for c in checks if c.required and not c.ok]
725
+ print()
726
+ print(f"{len(checks)} checks, {len(blocking)} blocking: {[c.name for c in blocking]}")
727
+ """)
728
+
729
+ term("cli-doctor", "Preflight",
730
+ "python -m tiny_computer_agent --doctor",
731
+ ["doctor", "display_available", "detector_available"],
732
+ note="The first thing to run on a new machine. Exits non-zero when not ready.",
733
+ argv=["{py}", "-m", "tiny_computer_agent", "--doctor"])
734
+
735
+ term("build-agent", "Preflight",
736
+ "One call wires screen + detector + brain + executor + policy",
737
+ ["build_agent", "Policy", "AgentLoop"],
738
+ note="Defaults are the safe ones: dry run, a step budget, and a kill-switch file.",
739
+ code="""
740
+ from tiny_computer_agent import Element, FakeScreen, PlannerBrain, build_agent
741
+ loop = build_agent(
742
+ brain=PlannerBrain(),
743
+ screen=FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)]),
744
+ dry_run=True, max_steps=4,
745
+ )
746
+ print("build_agent(...) ->", type(loop).__name__)
747
+ print(" executor :", type(loop.executor).__name__, "| dry_run:", loop.executor.dry_run)
748
+ print(" step budget :", loop.policy.step_budget)
749
+ print(" kill switch :", loop.policy.kill_switch_path)
750
+ print(" grid clicks :", loop.policy.allow_grid_clicks)
751
+ print()
752
+ print(loop.run("click 'Save'").render())
753
+ """)
754
+
755
+ # =========================================================================== #
756
+ # 9. the model backends
757
+ # =========================================================================== #
758
+
759
+ term("models-deps", "Models",
760
+ "Which model runtime is present, and are the weights cached?",
761
+ ["model_dependencies", "model_cached", "MODEL_BRAINS", "make_model_brain",
762
+ "DEFAULT_TEXT_MODEL", "DEFAULT_VISION_MODEL", "BrainError"],
763
+ note="Weights are loaded lazily, so asking is free.",
764
+ code="""
765
+ from tiny_computer_agent import (DEFAULT_TEXT_MODEL, DEFAULT_VISION_MODEL, MODEL_BRAINS,
766
+ BrainError, make_model_brain, model_cached, model_dependencies)
767
+ ok, detail = model_dependencies()
768
+ print("model_dependencies() ->", ok, "|", detail)
769
+ print("default text model :", DEFAULT_TEXT_MODEL)
770
+ print(" weights cached :", model_cached(DEFAULT_TEXT_MODEL))
771
+ print("default vision model :", DEFAULT_VISION_MODEL)
772
+ print(" weights cached :", model_cached(DEFAULT_VISION_MODEL))
773
+ print()
774
+ print("brains by name, and an honest refusal for the rest:")
775
+ for name in MODEL_BRAINS:
776
+ print(f" make_model_brain({name!r:13}) -> {type(make_model_brain(name)).__name__}")
777
+ try:
778
+ make_model_brain("gpt-9")
779
+ except BrainError as exc:
780
+ print(" make_model_brain('gpt-9') -> BrainError:", exc)
781
+ """)
782
+
783
+ term("models-device", "Models",
784
+ "Device and dtype selection",
785
+ ["resolve_device", "pick_dtype"],
786
+ note="CUDA if it is there, CPU otherwise; the harness never needs a GPU.",
787
+ code="""
788
+ from tiny_computer_agent import pick_dtype, resolve_device
789
+ print("resolve_device() ->", resolve_device())
790
+ print("resolve_device('cpu') ->", resolve_device("cpu"))
791
+ print("pick_dtype('cpu') ->", pick_dtype("cpu"))
792
+ print("pick_dtype('cuda') ->", pick_dtype("cuda"))
793
+ """)
794
+
795
+ term("models-prompt", "Models",
796
+ "The prompt a model actually receives - and how it learns from a dead end",
797
+ ["ModelBrain", "ModelBrain.build_prompt", "ModelBrain.decide"],
798
+ note="Placeholders, not worked examples: a weak model echoes the prompt, and an echo fails validation.",
799
+ code="""
800
+ from tiny_computer_agent import Element, ModelBrain, Observation
801
+
802
+ class Scripted(ModelBrain):
803
+ name = "scripted"
804
+ def _generate(self, prompt, observation):
805
+ self.prompt_seen = prompt
806
+ return '{"kind": "click", "index": 2, "reason": "saves the document"}'
807
+
808
+ brain = Scripted("example/model")
809
+ frame = Observation(text="Untitled document - unsaved changes", elements=(
810
+ Element("Files", x=10, y=6, width=60, height=20),
811
+ Element("Save", x=200, y=270, width=110, height=30),
812
+ Element("Cancel", x=400, y=270, width=110, height=30),
813
+ ))
814
+ action = brain.decide("save the document", frame)
815
+ print("the model chose:", action.describe())
816
+ print()
817
+ print("the prompt it was given:")
818
+ print(brain.prompt_seen)
819
+ """)
820
+
821
+ term("models-prompt-history", "Models",
822
+ "A second decision is told what already failed",
823
+ ["ModelBrain", "ModelBrain.build_prompt", "ModelBrain.history"],
824
+ note="'you already tried this and the screen did NOT change' is the most useful line a small model gets.",
825
+ code="""
826
+ from tiny_computer_agent import Element, ModelBrain, Observation
827
+
828
+ class Scripted(ModelBrain):
829
+ name = "scripted"
830
+ def _generate(self, prompt, observation):
831
+ self.prompt_seen = prompt
832
+ return '{"kind": "click", "index": 2, "reason": "again"}'
833
+
834
+ brain = Scripted("example/model")
835
+ frame = Observation(text="Untitled document", elements=(
836
+ Element("Files", x=10, y=6, width=60, height=20),
837
+ Element("Save", x=200, y=270, width=110, height=30),
838
+ ))
839
+ brain.decide("save", frame) # first attempt
840
+ brain.decide("save", frame) # same screen: nothing changed
841
+ print("history the brain is carrying:", brain.history)
842
+ print()
843
+ print("the tail of the second prompt:")
844
+ print(brain.prompt_seen[-420:])
845
+ """)
846
+
847
+ # =========================================================================== #
848
+ # 10. traces and scoring
849
+ # =========================================================================== #
850
+
851
+ term("scenario", "Traces & scoring",
852
+ "A task with one right answer, and the frames to show",
853
+ ["Scenario", "Scenario.screen", "run_scenario", "Scenario.label"],
854
+ note="Ground truth lives with the task, so a run can be scored at all.",
855
+ code="""
856
+ from tiny_computer_agent import Element, PlannerBrain, Scenario, run_scenario
857
+
858
+ def item(label, x): return Element(label, x=x, y=6, width=80, height=20)
859
+ def button(label, x): return Element(label, x=x, y=270, width=110, height=30)
860
+
861
+ scenario = Scenario(
862
+ "click 'Save' to save the document",
863
+ frames=[(item("Files", 10), item("Edit", 100)),
864
+ (Element("Name", x=200, y=195, width=200, height=26), button("Save", 200)),
865
+ (item("Files", 10), button("Save", 200))],
866
+ expected="Save", name="save the document",
867
+ )
868
+ print("Scenario:", scenario.label, "| frames:", len(scenario.frames), "| the right answer:", scenario.expected)
869
+ print()
870
+ trace = run_scenario(scenario, PlannerBrain())
871
+ print(trace.render())
872
+ """)
873
+
874
+ term("trace-record", "Traces & scoring",
875
+ "An episode becomes a portable, pixel-free trace",
876
+ ["Trace", "Trace.record", "Trace.save", "Trace.load", "Trace.to_json",
877
+ "Trace.to_dict", "Trace.choice", "Trace.decided", "Trace.frames"],
878
+ note="JSON, so it diffs in review and replays on a machine with nothing installed.",
879
+ code="""
880
+ import json, tempfile
881
+ from pathlib import Path
882
+ from tiny_computer_agent import Element, PlannerBrain, Scenario, Trace, run_scenario
883
+
884
+ scenario = Scenario("click 'Save'", frames=[(Element("Save", x=320, y=120, width=80, height=24),)],
885
+ expected="Save", name="save")
886
+ trace = run_scenario(scenario, PlannerBrain())
887
+ print("frames:", trace.frames, "| choice:", trace.choice,
888
+ "| decided:", trace.decided, "| blocked:", trace.blocked)
889
+ path = Path(tempfile.mkdtemp()) / "save.json"
890
+ trace.save(path)
891
+ print("saved:", path.name, f"({path.stat().st_size} bytes) | reloaded identical:",
892
+ Trace.load(path).to_dict() == trace.to_dict())
893
+ print()
894
+ print(json.dumps(trace.to_dict(), indent=2)[:560] + " ...")
895
+ """)
896
+
897
+ term("trace-replay", "Traces & scoring",
898
+ "Replay a recorded trace against a different brain",
899
+ ["Trace.replay", "Trace.observations", "score_trace"],
900
+ note="The frames are the fixture; the brain is the only variable. Nothing is captured or clicked.",
901
+ code="""
902
+ from tiny_computer_agent import Action, Element, PlannerBrain, Scenario, run_scenario, score_trace
903
+
904
+ scenario = Scenario("click 'Save'", frames=[
905
+ (Element("Files", x=10, y=6, width=60, height=20),),
906
+ (Element("Save", x=320, y=120, width=80, height=24),)], expected="Save", name="save")
907
+
908
+ recorded = run_scenario(scenario, PlannerBrain())
909
+ print("recorded by the planner :", recorded.choice, "->", score_trace(recorded).correct)
910
+
911
+ class AlwaysFirst:
912
+ name = "first-item"
913
+ def decide(self, goal, observation):
914
+ return Action("click", label=observation.elements[0].label, reason="first item")
915
+
916
+ replayed = recorded.replay(AlwaysFirst())
917
+ print("the same frames, another brain:", replayed.choice, "->", score_trace(replayed).correct)
918
+ print()
919
+ print("replayed frames carried over:", replayed.frames, "| ground truth carried over:", replayed.expected)
920
+ """)
921
+
922
+ term("score-trace", "Traces & scoring",
923
+ "Counting how often the choice was right",
924
+ ["score_trace", "score", "score_replay", "Verdict", "Verdict.line"],
925
+ note="Scored on normalised text, so OCR spacing is forgiven - the same bargain the index makes.",
926
+ code="""
927
+ from tiny_computer_agent import (Action, Element, PlannerBrain, Scenario, Trace, TraceStep,
928
+ score, score_replay, score_trace)
929
+ scenario = Scenario("click 'Save'", frames=[(Element("Save", x=320, y=120, width=80, height=24),)],
930
+ expected="Save", name="save")
931
+ print("score(scenario, PlannerBrain()) ->", score(scenario, PlannerBrain()).line().strip())
932
+ print()
933
+ print("a choice the OCR rendered as 'Save .' is still credited for 'Save':")
934
+ print(" ", score_trace(Trace(goal="g", expected="Save",
935
+ steps=[TraceStep(0, action=Action("click", label="Save ."))])).line().strip())
936
+ print()
937
+ print("a trace with no ground truth says so instead of inventing a verdict:")
938
+ print(" ", score_trace(Trace(goal="g",
939
+ steps=[TraceStep(0, action=Action("click", label="Save"))])).line().strip())
940
+ print()
941
+ print("score_replay(trace, another_brain) is the single-trace form of the roadmap step:")
942
+ print(" ", score_replay(Trace(goal="g", expected="Save",
943
+ steps=[TraceStep(0, action=Action("click", label="Cancel"))]), PlannerBrain()).line().strip())
944
+ """)
945
+
946
+ term("scoreboard", "Traces & scoring",
947
+ "Verdicts collected into an accuracy",
948
+ ["Scoreboard", "Scoreboard.render", "Scoreboard.accuracy", "Scoreboard.by_brain",
949
+ "Scoreboard.to_rows", "Scoreboard.confusion", "Scoreboard.blocked"],
950
+ note="The number the whole module exists for: which brain is right, and how often.",
951
+ code="""
952
+ from tiny_computer_agent import Action, Element, MockBrain, PlannerBrain, Scenario, benchmark
953
+
954
+ class FirstItem:
955
+ name = "first-item"
956
+ def decide(self, goal, observation):
957
+ return Action("click", label=observation.elements[0].label, reason="first item")
958
+
959
+ def scenario(goal, expected, frames):
960
+ return Scenario(goal, frames=frames, expected=expected, name=expected.lower())
961
+
962
+ suite = [
963
+ scenario("click 'Save' to save the document", "Save",
964
+ [(Element("Files", x=10, y=6, width=60, height=20),),
965
+ (Element("Save", x=320, y=120, width=80, height=24),)]),
966
+ scenario("click 'Cancel' to discard the change", "Cancel",
967
+ [(Element("Save", x=200, y=270, width=110, height=30),
968
+ Element("Cancel", x=400, y=270, width=110, height=30),)]),
969
+ ]
970
+ board = benchmark(suite, {
971
+ "planner": lambda s: PlannerBrain(),
972
+ "first-item": lambda s: FirstItem(),
973
+ "oracle": lambda s: MockBrain([{"kind": "click", "label": s.expected}]),
974
+ })
975
+ print(board.render())
976
+ print()
977
+ print("accuracy:", f"{board.accuracy:.0%}", "| by brain:", board.by_brain())
978
+ print("as rows :", board.to_rows())
979
+ print("confusion (expected, chose) -> count:", board.confusion())
980
+ """)
981
+
982
+ term("benchmark", "Traces & scoring",
983
+ "Every scenario through every brain, in one table",
984
+ ["benchmark", "Scoreboard.to_rows"],
985
+ note="A fresh brain per task, so state like 'already clicked this' cannot leak between them.",
986
+ code="""
987
+ from tiny_computer_agent import Action, Element, PlannerBrain, Scenario, benchmark
988
+
989
+ class FirstItem:
990
+ name = "first-item"
991
+ def decide(self, goal, observation):
992
+ return Action("click", label=observation.elements[0].label, reason="first item")
993
+
994
+ suite = [Scenario(f"click {label!r}", frames=[(Element("Files", x=10, y=6, width=60, height=20),
995
+ Element(label, x=200, y=270, width=110, height=30),)],
996
+ expected=label, name=label.lower())
997
+ for label in ("Save", "Cancel")]
998
+ board = benchmark(suite, {"planner": lambda s: PlannerBrain(), "first-item": lambda s: FirstItem()})
999
+ for row in board.to_rows():
1000
+ print(f" {row['brain']:12} {row['scenario']:8} expected={row['expected']:7} "
1001
+ f"chose={row['chose']:7} correct={row['correct']}")
1002
+ """)
1003
+
1004
+ term("benchmark-traces", "Traces & scoring",
1005
+ "Replay a whole recorded suite against one brain",
1006
+ ["benchmark_traces", "load_traces", "Trace.save"],
1007
+ note="The literal roadmap step: the frames are fixed, nothing new is captured.",
1008
+ code="""
1009
+ import tempfile
1010
+ from pathlib import Path
1011
+ from tiny_computer_agent import (Element, PlannerBrain, Scenario, benchmark_traces,
1012
+ load_traces, run_scenario)
1013
+
1014
+ def scenario(goal, expected, frames):
1015
+ return Scenario(goal, frames=frames, expected=expected, name=expected.lower())
1016
+
1017
+ suite = [
1018
+ scenario("click 'Save'", "Save", [(Element("Files", x=10, y=6, width=60, height=20),),
1019
+ (Element("Save", x=320, y=120, width=80, height=24),)]),
1020
+ scenario("click 'Cancel'", "Cancel", [(Element("Save", x=200, y=270, width=110, height=30),
1021
+ Element("Cancel", x=400, y=270, width=110, height=30),)]),
1022
+ ]
1023
+ folder = Path(tempfile.mkdtemp())
1024
+ for item in suite:
1025
+ run_scenario(item, PlannerBrain()).save(folder / f"{item.name}.json")
1026
+ print("recorded suite on disk:", [p.name for p in sorted(folder.glob("*.json"))])
1027
+ traces = load_traces(folder)
1028
+ print("load_traces ->", [t.name for t in traces])
1029
+ print()
1030
+ board = benchmark_traces(traces, PlannerBrain(), name="planner (replayed)")
1031
+ print(board.render())
1032
+ """)
1033
+
1034
+ # =========================================================================== #
1035
+ # 11. the command line
1036
+ # =========================================================================== #
1037
+
1038
+ term("cli-image", "Command line",
1039
+ "--image example (replay a screenshot, dry run)",
1040
+ ["main", "build_parser", "ImageScreen", "detector_available"],
1041
+ note="No display needed: the whole pipeline over a still image.",
1042
+ argv=["{py}", "-m", "tiny_computer_agent", "--image", "example"])
1043
+
1044
+ term("cli-script", "Command line",
1045
+ "--script (replay a fixed action list instead of a brain)",
1046
+ ["MockBrain", "parse_action"],
1047
+ note="The script is validated exactly like a model's answer: nothing skips the gates.",
1048
+ argv=["{py}", "-m", "tiny_computer_agent", "--script",
1049
+ '[{"kind": "click", "label": "Save"}, {"kind": "done", "reason": "finished"}]'])
1050
+
1051
+ term("cli-verify-text", "Command line",
1052
+ "--verify-text (the harness decides success, not the brain)",
1053
+ ["AgentLoop.run", "Episode.success"],
1054
+ note="Without it, an action that removes its own target looks like a failure.",
1055
+ argv=["{py}", "-m", "tiny_computer_agent", "--script",
1056
+ '[{"kind": "wait", "seconds": 0.1}]', "--verify-text", "Unsaved", "--max-steps", "2"])
1057
+
1058
+ term("cli-trace", "Command line",
1059
+ "--trace / --expected (record the run, then score it)",
1060
+ ["Trace.record", "Trace.save", "score_trace"],
1061
+ note="Writes JSON next to the code, and prints the score straight away.",
1062
+ argv=["{py}", "-m", "tiny_computer_agent", "--image", "example",
1063
+ "--trace", "/tmp/gallery-save.json", "--expected", "Save"])
1064
+
1065
+ term("cli-detector", "Command line",
1066
+ "--no-ocr (index a numbered grid instead)",
1067
+ ["GridDetector", "make_detector", "detector_available"],
1068
+ note="Useful on a machine with no OCR engine, or for a canvas app.",
1069
+ argv=["{py}", "-m", "tiny_computer_agent", "--image", "example", "--no-ocr"])
1070
+
1071
+ term("cli-exit-codes", "Command line",
1072
+ "Exit codes: 0 success, 1 failure, 2 bad usage, 3 no screen",
1073
+ ["main"],
1074
+ note="A shell can tell 'it worked' from 'it gave up' without parsing any text.",
1075
+ code="""
1076
+ import subprocess, sys
1077
+ cases = [
1078
+ (["--script", '[{"kind": "done", "reason": "nothing to do"}]'], "a brain-reported success"),
1079
+ (["click 'Nonexistent button'"], "a goal that cannot be reached"),
1080
+ (["--real-input"], "real input without the allow env"),
1081
+ (["--brain", "vlm"], "the vlm brain with no pixels"),
1082
+ (["--image", "/nonexistent/shot.png"], "a screenshot that is not there"),
1083
+ ]
1084
+ for flags, what in cases:
1085
+ code = subprocess.run([sys.executable, "-m", "tiny_computer_agent", *flags],
1086
+ capture_output=True, text=True).returncode
1087
+ print(f" exit {code} {what:38} ({' '.join(flags)[:46]})")
1088
+ """, env={"TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT": ""})
1089
+
1090
+ term("cli-refusals", "Command line",
1091
+ "The two deliberate acts before the mouse can move",
1092
+ ["main", "ALLOW_REAL_INPUT_ENV"],
1093
+ note="--real-input without the env var, and --brain vlm with no pixels: both refuse.",
1094
+ code="""
1095
+ import subprocess, sys
1096
+ for flags in (["--real-input"], ["--brain", "vlm"]):
1097
+ print(f"$ python -m tiny_computer_agent {' '.join(flags)}")
1098
+ subprocess.run([sys.executable, "-m", "tiny_computer_agent", *flags])
1099
+ print()
1100
+ """, env={"TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT": ""})
1101
+
1102
+ term("cli-live-headless", "Command line",
1103
+ "--live with no display is refused, with the fix",
1104
+ ["display_available", "main"],
1105
+ note="It tells you what to do instead of dying inside an episode.",
1106
+ argv=["{py}", "-m", "tiny_computer_agent", "--live"],
1107
+ env={"DISPLAY": "", "WAYLAND_DISPLAY": ""})
1108
+
1109
+ term("cli-live-dry-run", "Command line",
1110
+ "--live (your real screen, dry run)",
1111
+ ["GrabScreen", "IndexedScreen", "AutoDetector", "main"],
1112
+ note="Real capture, real OCR, a real element index - and not one click.",
1113
+ argv=["{py}", "-m", "tiny_computer_agent", "--live", "--max-steps", "2"],
1114
+ env={"DISPLAY": ":99", "TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT": ""})
1115
+
1116
+
1117
+ # =========================================================================== #
1118
+ # 12. on a real desktop: real capture, real OCR, a real click
1119
+ # =========================================================================== #
1120
+
1121
+ SCENES["dialog"] = (ROOT / "showcase_scenes" / "dialog.py").read_text()
1122
+ SCENES["canvas"] = (ROOT / "showcase_scenes" / "canvas.py").read_text()
1123
+ SCENES["toolbar"] = (ROOT / "showcase_scenes" / "toolbar.py").read_text()
1124
+
1125
+ DIALOG_BOX = (100, 100, 600, 380)
1126
+ TOOLBAR_BOX = (140, 140, 820, 480)
1127
+
1128
+
1129
+ def make_capture(tmp):
1130
+ proc = open_scene("dialog", tmp)
1131
+ try:
1132
+ return annotate(grab(), [], title="GrabScreen(monitor=1).capture() - a real 1280x800 desktop")
1133
+ finally:
1134
+ close_scene(proc)
1135
+
1136
+
1137
+ def make_ocr_index(tmp):
1138
+ from tiny_computer_agent import OcrDetector
1139
+ proc = open_scene("dialog", tmp)
1140
+ try:
1141
+ frame = grab()
1142
+ elements = OcrDetector().detect(frame)
1143
+ return annotate(frame, elements, title="OcrDetector().detect(frame) -> "
1144
+ + str(len(elements)) + " labelled elements with real coordinates")
1145
+ finally:
1146
+ close_scene(proc)
1147
+
1148
+
1149
+ def make_click(tmp):
1150
+ from tiny_computer_agent import (AgentLoop, GrabScreen, IndexedScreen, OcrDetector,
1151
+ PlannerBrain, Policy, PyAutoGuiExecutor)
1152
+ proc = open_scene("dialog", tmp)
1153
+ try:
1154
+ screen = IndexedScreen(GrabScreen(monitor=1), OcrDetector())
1155
+ before_observation = screen.capture()
1156
+ before = crop(annotate(grab(), before_observation.elements), DIALOG_BOX)
1157
+ episode = AgentLoop(
1158
+ screen=screen, brain=PlannerBrain(), executor=PyAutoGuiExecutor(),
1159
+ policy=Policy(dry_run=False, step_budget=3),
1160
+ verify=lambda observation: "saved" in observation.text.lower(),
1161
+ ).run("click 'Save' to save the document")
1162
+ target = before_observation.find("Save")
1163
+ after = crop(annotate(grab(), [], click=target.center if target else None), DIALOG_BOX)
1164
+ lines = ["result: " + ("success" if episode.success else "failure")
1165
+ + " - " + episode.stop_reason]
1166
+ lines += [" " + step.line() for step in episode.steps][:2]
1167
+ return overlay_lines(side_by_side(
1168
+ before, after, title="PyAutoGuiExecutor: a real click lands on a real button"), lines)
1169
+ finally:
1170
+ close_scene(proc)
1171
+
1172
+
1173
+ def make_verify(tmp):
1174
+ from tiny_computer_agent import (AgentLoop, GrabScreen, IndexedScreen, OcrDetector,
1175
+ PlannerBrain, Policy, PyAutoGuiExecutor)
1176
+ proc = open_scene("dialog", tmp)
1177
+ try:
1178
+ episode = AgentLoop(
1179
+ screen=IndexedScreen(GrabScreen(monitor=1), OcrDetector()),
1180
+ brain=PlannerBrain(), executor=PyAutoGuiExecutor(),
1181
+ policy=Policy(dry_run=False, step_budget=3),
1182
+ verify=lambda observation: "saved" in observation.text.lower(),
1183
+ ).run("click 'Save' to save the document")
1184
+ marker = tmp / "dialog.marker"
1185
+ frame = crop(annotate(grab(), []), DIALOG_BOX)
1186
+ return overlay_lines(frame, [
1187
+ "the window changed its own title to 'Saved to disk',",
1188
+ "and wrote " + repr(marker.read_text()) + " to " + marker.name,
1189
+ "verify(...) saw it -> episode.success = " + str(episode.success),
1190
+ "stop reason: " + episode.stop_reason,
1191
+ ])
1192
+ finally:
1193
+ close_scene(proc)
1194
+
1195
+
1196
+ def make_grid_fallback(tmp):
1197
+ from tiny_computer_agent import AutoDetector, OcrDetector
1198
+ proc = open_scene("canvas", tmp)
1199
+ try:
1200
+ frame = grab()
1201
+ ocr = OcrDetector().detect(frame)
1202
+ elements = AutoDetector().detect(frame)
1203
+ return overlay_lines(annotate(
1204
+ frame, elements, title="a canvas app has no text: the grid fallback"), [
1205
+ "OcrDetector found " + str(len(ocr)) + " labels on these pixels",
1206
+ "AutoDetector fell back to a " + str(len(elements))
1207
+ + "-cell numbered grid, so the agent still has moves",
1208
+ "with real input a blind grid click is refused unless --allow-grid is passed",
1209
+ ])
1210
+ finally:
1211
+ close_scene(proc)
1212
+
1213
+
1214
+ def make_deny(tmp):
1215
+ from tiny_computer_agent import Action, Observation, OcrDetector, Policy, PolicyViolation
1216
+ proc = open_scene("toolbar", tmp)
1217
+ try:
1218
+ frame = grab()
1219
+ elements = OcrDetector().detect(frame)
1220
+ target = next((e for e in elements if e.matches("Terminal")), None)
1221
+ message = "no Terminal button on screen"
1222
+ if target is not None:
1223
+ try:
1224
+ Policy(dry_run=False).check(
1225
+ Action("click", label=target.label), Observation(elements=tuple(elements)))
1226
+ message = "allowed (unexpected)"
1227
+ except PolicyViolation as exc:
1228
+ message = "refused: " + str(exc)
1229
+ return overlay_lines(crop(annotate(
1230
+ frame, elements,
1231
+ title="the harness can see a Terminal button - and will not press it"), TOOLBAR_BOX),
1232
+ ["Policy(dry_run=False).check(click 'Terminal')", message])
1233
+ finally:
1234
+ close_scene(proc)
1235
+
1236
+
1237
+ def make_cli_real_input(tmp):
1238
+ from tiny_computer_agent import OcrDetector
1239
+ proc = open_scene("dialog", tmp)
1240
+ try:
1241
+ elements = OcrDetector().detect(grab())
1242
+ before = crop(annotate(grab(), elements), DIALOG_BOX)
1243
+ result = subprocess.run(
1244
+ [PY, "-m", "tiny_computer_agent", "--live", "--real-input", "--verify-text", "Saved",
1245
+ "click 'Save' to save the document"],
1246
+ capture_output=True, text=True,
1247
+ env=dict(os.environ, TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT="1"))
1248
+ after = crop(grab(), DIALOG_BOX)
1249
+ lines = [line for line in result.stdout.splitlines() if line.strip()][:3]
1250
+ return overlay_lines(side_by_side(
1251
+ before, after,
1252
+ title="the CLI driving the real mouse: --live --real-input --verify-text Saved"), lines)
1253
+ finally:
1254
+ close_scene(proc)
1255
+
1256
+
1257
+ desk("desk-capture", "Real desktop",
1258
+ "GrabScreen: capture a real desktop",
1259
+ ["GrabScreen", "GrabScreen.capture", "ScreenUnavailable"],
1260
+ note="1280x800 of a real X server. mss first, Pillow's ImageGrab as the fallback.",
1261
+ scene_name="dialog", make=make_capture)
1262
+
1263
+ desk("desk-ocr-index", "Real desktop",
1264
+ "The element index, drawn on the pixels it came from",
1265
+ ["OcrDetector", "OcrDetector.detect", "IndexedScreen", "Element.center"],
1266
+ note="Every box is a real OCR word with a real screen coordinate: the model picks a label, not a pixel.",
1267
+ scene_name="dialog", make=make_ocr_index)
1268
+
1269
+ desk("desk-click", "Real desktop",
1270
+ "A real click, and the window's own reaction",
1271
+ ["PyAutoGuiExecutor", "AgentLoop.run", "Policy.dry_run", "resolve_point"],
1272
+ note="Pixels in, a label chosen, a pixel resolved, a button pressed - and the dialog closed itself.",
1273
+ scene_name="dialog", make=make_click)
1274
+
1275
+ desk("desk-verify", "Real desktop",
1276
+ "Success only when the harness can see it",
1277
+ ["AgentLoop.run", "Episode.success", "Episode.stop_reason"],
1278
+ note="The app changed its own title and wrote a marker file; the verifier saw the change.",
1279
+ scene_name="dialog", make=make_verify)
1280
+
1281
+ desk("desk-grid-fallback", "Real desktop",
1282
+ "A screen with no text still has legal moves",
1283
+ ["AutoDetector", "GridDetector", "OcrDetector"],
1284
+ note="OCR finds nothing on a canvas, so the index degrades to numbered cells - and says so.",
1285
+ scene_name="canvas", make=make_grid_fallback)
1286
+
1287
+ desk("desk-policy-refusal", "Real desktop",
1288
+ "The safety gate, on a real button, in a real window",
1289
+ ["Policy.check", "Policy.deny_labels", "PolicyViolation", "Observation.find"],
1290
+ note="The Terminal button is visible and labelled - and the policy refuses to press it.",
1291
+ scene_name="toolbar", make=make_deny)
1292
+
1293
+ desk("desk-cli-real-input", "Real desktop",
1294
+ "The same click, driven end to end from the CLI",
1295
+ ["main", "PyAutoGuiExecutor", "AgentLoop.run"],
1296
+ note="--live --real-input --verify-text Saved: capture, OCR, click, verify, exit code.",
1297
+ scene_name="dialog", make=make_cli_real_input)
1298
+
1299
+ # =========================================================================== #
1300
+ # 13. the pieces a user swaps out
1301
+ # =========================================================================== #
1302
+
1303
+ term("protocols", "Extensibility",
1304
+ "Every stage is a Protocol: bring your own screen, brain, detector, executor",
1305
+ ["Brain", "Brain.decide", "ScreenSource", "ScreenSource.capture", "Detector",
1306
+ "Detector.detect", "Executor", "Executor.execute", "ActionResult"],
1307
+ note="The loop depends on four one-method contracts, not on the shipped implementations.",
1308
+ code="""
1309
+ from tiny_computer_agent import ActionResult, Action, AgentLoop, Element, Observation
1310
+
1311
+
1312
+ class MyScreen: # ScreenSource: capture() -> Observation
1313
+ def capture(self):
1314
+ return Observation(elements=(Element("Deploy", x=10, y=10, width=90, height=30),),
1315
+ text="release ready")
1316
+
1317
+
1318
+ class MyBrain: # Brain: decide(goal, observation) -> Action
1319
+ def __init__(self):
1320
+ self.turns = 0
1321
+
1322
+ def decide(self, goal, observation):
1323
+ self.turns += 1
1324
+ if self.turns > 1:
1325
+ return Action("done", reason="the release is deployed")
1326
+ return Action("click", label="Deploy", reason="the goal names it")
1327
+
1328
+
1329
+ class MyDetector: # Detector: detect(image) -> tuple[Element, ...]
1330
+ def detect(self, image):
1331
+ return (Element("Deploy", x=10, y=10, width=90, height=30),)
1332
+
1333
+
1334
+ class MyExecutor: # Executor: execute(action, observation) -> ActionResult
1335
+ def __init__(self):
1336
+ self.saw = []
1337
+
1338
+ def execute(self, action, observation=None):
1339
+ self.saw.append(action.describe())
1340
+ return ActionResult(action=action, ok=True, note="my own executor ran it")
1341
+
1342
+
1343
+ executor = MyExecutor()
1344
+ episode = AgentLoop(screen=MyScreen(), brain=MyBrain(), executor=executor).run("deploy the release")
1345
+ print(episode.render())
1346
+ print()
1347
+ print("and what my executor was handed:", executor.saw)
1348
+ """)
1349
+
1350
+ term("brain-mock", "Decisions",
1351
+ "MockBrain: replay a fixed script, exactly once",
1352
+ ["MockBrain", "MockBrain.decide", "MockBrain.remaining"],
1353
+ note="Deterministic and offline - the workhorse for tests, demos and replays.",
1354
+ code="""
1355
+ from tiny_computer_agent import Element, MockBrain, Observation
1356
+
1357
+ brain = MockBrain([
1358
+ {"kind": "click", "label": "Save"},
1359
+ {"kind": "key", "keys": "ctrl+s"},
1360
+ {"kind": "done", "reason": "the document is saved"},
1361
+ ])
1362
+ frame = Observation(elements=(Element("Save", x=320, y=120, width=80, height=24),))
1363
+
1364
+ print("scripted actions:", 3)
1365
+ while brain.remaining:
1366
+ action = brain.decide("save the document", frame)
1367
+ print(f" remaining={brain.remaining} -> {action.describe()}")
1368
+ print()
1369
+ print("with the script exhausted it stops claiming work it has not planned:")
1370
+ print(" ", brain.decide("save the document", frame).describe())
1371
+ """)
1372
+
1373
+ term("brain-planner", "Decisions",
1374
+ "PlannerBrain: no weights, no download, and it gives up honestly",
1375
+ ["PlannerBrain", "PlannerBrain.decide", "GoalUnreachable"],
1376
+ note="The proof the harness works end to end, and a real fallback when no model is wanted.",
1377
+ code="""
1378
+ from tiny_computer_agent import Element, FakeScreen, GoalUnreachable, PlannerBrain
1379
+
1380
+ brain = PlannerBrain()
1381
+ goal = "click 'Save' to save the document"
1382
+ frames = FakeScreen([(Element("Files", x=10, y=6, width=60, height=20),),
1383
+ (Element("Save", x=320, y=120, width=80, height=24),)])
1384
+
1385
+ for step in range(3):
1386
+ observation = frames.capture()
1387
+ chosen = brain.decide(goal, observation)
1388
+ print(f" step {step}: {chosen.describe()}")
1389
+
1390
+ print()
1391
+ print("when the target never appears it gives up rather than flailing:")
1392
+ try:
1393
+ PlannerBrain(max_waits=0).decide(goal, FakeScreen([(Element("Files"),)]).capture())
1394
+ except GoalUnreachable as exc:
1395
+ print(" GoalUnreachable:", exc)
1396
+ """)
1397
+
1398
+ term("models-swap", "Models",
1399
+ "The two real model backends, and lazy loading",
1400
+ ["TextPlannerBrain", "VLMBrain", "TextPlannerBrain.unload", "VLMBrain.unload",
1401
+ "ModelBrain.loaded", "make_model_brain"],
1402
+ note="Constructing a brain costs nothing; the weights arrive on the first decide().",
1403
+ code="""
1404
+ from tiny_computer_agent import make_model_brain
1405
+
1406
+ for name in ["text-planner", "vlm"]:
1407
+ brain = make_model_brain(name)
1408
+ print(f"{name:13} -> {type(brain).__name__}")
1409
+ print(f" model id : {brain.model_id}")
1410
+ print(f" weights loaded : {brain.loaded}")
1411
+ print(f" needs an index : {brain.requires_index}")
1412
+ print(f" menu size cap : {brain.max_menu_items} items")
1413
+ brain.unload()
1414
+ print(f" after unload() : loaded={brain.loaded}")
1415
+ print()
1416
+ print("neither download happens until decide(), which is what makes --doctor cheap")
1417
+ """)
1418
+
1419
+ term("fake-desktop", "Screens",
1420
+ "The built-in synthetic desktop",
1421
+ ["fake_desktop", "write_fake_desktop", "OcrDetector"],
1422
+ note="Draws a screenshot-like desktop offline - the same image --image example replays.",
1423
+ code="""
1424
+ import tempfile
1425
+ from pathlib import Path
1426
+ from tiny_computer_agent import OcrDetector, fake_desktop, write_fake_desktop
1427
+
1428
+ image = fake_desktop()
1429
+ print("fake_desktop() ->", type(image).__name__, image.size, image.mode)
1430
+ path = Path(tempfile.mkdtemp()) / "example.png"
1431
+ print("write_fake_desktop(path) ->", write_fake_desktop(path).name, f"({path.stat().st_size} bytes)")
1432
+ print()
1433
+ print("and OCR reads it like any other screenshot:")
1434
+ for element in OcrDetector().detect(image):
1435
+ print(f" {element.label!r:34} at {element.center}")
1436
+ """)
1437
+
1438
+ # =========================================================================== #
1439
+ # 14. closing the last gaps in the picture map
1440
+ # =========================================================================== #
1441
+
1442
+ # names the existing pictures already exercise but did not claim
1443
+ _AUGMENT = {
1444
+ "vocab-verbs": ["Action.to_dict"],
1445
+ "exec-refuses-unresolved": ["InputUnavailable"],
1446
+ "scenario": ["Trace.render"],
1447
+ "trace-record": ["Trace.name", "Trace.decision_step", "Trace.blocked", "TraceStep",
1448
+ "TraceStep.observation", "TraceStep.to_dict"],
1449
+ "scoreboard": ["Scoreboard.add", "Scoreboard.total", "Scoreboard.earned",
1450
+ "Scoreboard.accuracy_for", "Scoreboard.brains"],
1451
+ "cli-script": ["demo_screen"],
1452
+ "cli-doctor": ["report"],
1453
+ }
1454
+ for _shot in CATALOG:
1455
+ _extra = _AUGMENT.get(_shot.id)
1456
+ if _extra:
1457
+ _shot.shows = sorted(set(_shot.shows) | set(_extra))
1458
+
1459
+ # and the two demos whose output should actually print what they claim
1460
+ def _edit(sid, old, new):
1461
+ for _shot in CATALOG:
1462
+ if _shot.id == sid:
1463
+ assert old in _shot.code, f"{sid}: anchor not found"
1464
+ _shot.code = _shot.code.replace(old, new)
1465
+ return
1466
+ raise AssertionError(sid)
1467
+
1468
+ _edit("vocab-verbs",
1469
+ ' print(f" parsed -> {action.describe()}")',
1470
+ ' print(f" parsed -> {action.describe()}")\n'
1471
+ ' if payload["kind"] == "click":\n'
1472
+ ' print(f" as data: {action.to_dict()}")')
1473
+
1474
+ _edit("trace-record",
1475
+ 'print("frames:", trace.frames, "| choice:", trace.choice,',
1476
+ 'print("name:", trace.name, "| decision_step:", trace.decision_step().action.describe())\n'
1477
+ 'step = trace.steps[0]\n'
1478
+ 'labels = [e.label for e in step.observation().elements]\n'
1479
+ 'print("first step as data:", step.to_dict()["action"], "| labels:", labels)\n'
1480
+ 'print("frames:", trace.frames, "| choice:", trace.choice,')
1481
+
1482
+ _edit("scoreboard",
1483
+ 'print("accuracy:", f"{board.accuracy:.0%}", "| by brain:", board.by_brain())',
1484
+ 'print("total:", board.total, "| earned:", board.earned, "| accuracy:", f"{board.accuracy:.0%}")\n'
1485
+ 'print("brains:", board.brains(), "| accuracy_for(\'planner\'):", f"{board.accuracy_for(\'planner\'):.0%}")')
1486
+
1487
+ _edit("exec-refuses-unresolved",
1488
+ 'print()\nresolved = executor.execute(Action("done", reason="nothing to do"), frame)',
1489
+ 'print()\nfrom tiny_computer_agent import InputUnavailable\n'
1490
+ 'print("and when pyautogui itself is missing, it says so:")\n'
1491
+ 'print(" ", InputUnavailable("real input needs pyautogui and a live display: '
1492
+ 'pip install \'tiny-computer-agent[desktop]\'"))\nprint()\n'
1493
+ 'resolved = executor.execute(Action("done", reason="nothing to do"), frame)')