@widgex/wgx 0.0.0-stage → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE.txt +49 -0
  2. package/README.md +33 -2
  3. package/ThirdPartyNotices.txt +4685 -0
  4. package/package.json +30 -4
  5. package/provider-clis.json +1211 -0
  6. package/python/XACTOR.md +146 -0
  7. package/python/browser_actor_host.py +456 -0
  8. package/python/cdp_page.py +363 -0
  9. package/python/harness_check.py +77 -0
  10. package/python/requirements.txt +1 -0
  11. package/python/test_line_endings.py +185 -0
  12. package/python/xactor/__init__.py +18 -0
  13. package/python/xactor/__main__.py +226 -0
  14. package/python/xactor/api.py +882 -0
  15. package/python/xactor/bridge.py +784 -0
  16. package/python/xactor/dom.py +117 -0
  17. package/python/xactor/man/add_card.md +89 -0
  18. package/python/xactor/man/agent-vs-fork.md +108 -0
  19. package/python/xactor/man/agent.md +333 -0
  20. package/python/xactor/man/ask_model.md +110 -0
  21. package/python/xactor/man/ask_reference.md +56 -0
  22. package/python/xactor/man/ask_user.md +103 -0
  23. package/python/xactor/man/attach_app.md +595 -0
  24. package/python/xactor/man/attach_ide.md +65 -0
  25. package/python/xactor/man/attach_ide_surface.md +31 -0
  26. package/python/xactor/man/await_user.md +214 -0
  27. package/python/xactor/man/bash.md +142 -0
  28. package/python/xactor/man/browser-driver.md +137 -0
  29. package/python/xactor/man/browser_activity.md +53 -0
  30. package/python/xactor/man/browser_attach.md +84 -0
  31. package/python/xactor/man/browser_close_tab.md +50 -0
  32. package/python/xactor/man/browser_detach.md +50 -0
  33. package/python/xactor/man/browser_exec.md +66 -0
  34. package/python/xactor/man/browser_focus_tab.md +47 -0
  35. package/python/xactor/man/browser_hybrid.md +47 -0
  36. package/python/xactor/man/browser_open.md +50 -0
  37. package/python/xactor/man/browser_open_tab.md +58 -0
  38. package/python/xactor/man/browser_plumbing.md +107 -0
  39. package/python/xactor/man/browser_screenshot.md +56 -0
  40. package/python/xactor/man/browser_snapshot.md +52 -0
  41. package/python/xactor/man/browser_tabs.md +67 -0
  42. package/python/xactor/man/bulletin.md +88 -0
  43. package/python/xactor/man/capture_page.md +54 -0
  44. package/python/xactor/man/cd.md +64 -0
  45. package/python/xactor/man/chess_move.md +53 -0
  46. package/python/xactor/man/children.md +89 -0
  47. package/python/xactor/man/close_task.md +14 -0
  48. package/python/xactor/man/compact.md +75 -0
  49. package/python/xactor/man/drive.md +140 -0
  50. package/python/xactor/man/drive_space.md +72 -0
  51. package/python/xactor/man/driving.md +168 -0
  52. package/python/xactor/man/edit_file.md +112 -0
  53. package/python/xactor/man/finding.md +194 -0
  54. package/python/xactor/man/fork_agent.md +45 -0
  55. package/python/xactor/man/glob.md +83 -0
  56. package/python/xactor/man/grep.md +115 -0
  57. package/python/xactor/man/harness.md +165 -0
  58. package/python/xactor/man/harness_check.md +70 -0
  59. package/python/xactor/man/harness_format.md +257 -0
  60. package/python/xactor/man/highlight.md +66 -0
  61. package/python/xactor/man/instruct.md +111 -0
  62. package/python/xactor/man/ls.md +63 -0
  63. package/python/xactor/man/man.md +86 -0
  64. package/python/xactor/man/merge_wiz.md +40 -0
  65. package/python/xactor/man/notebook.md +98 -0
  66. package/python/xactor/man/overview.md +89 -0
  67. package/python/xactor/man/permissions.md +137 -0
  68. package/python/xactor/man/phase.md +150 -0
  69. package/python/xactor/man/powershell.md +74 -0
  70. package/python/xactor/man/profiles.md +60 -0
  71. package/python/xactor/man/project_drive.md +49 -0
  72. package/python/xactor/man/read_bulletin.md +89 -0
  73. package/python/xactor/man/read_file.md +90 -0
  74. package/python/xactor/man/relay.md +216 -0
  75. package/python/xactor/man/reply.md +72 -0
  76. package/python/xactor/man/reply_message.md +78 -0
  77. package/python/xactor/man/report_bug.md +118 -0
  78. package/python/xactor/man/request_answer.md +142 -0
  79. package/python/xactor/man/request_merge_decisions.md +121 -0
  80. package/python/xactor/man/request_operator_action.md +152 -0
  81. package/python/xactor/man/request_review.md +122 -0
  82. package/python/xactor/man/request_user_action.md +126 -0
  83. package/python/xactor/man/return_card.md +71 -0
  84. package/python/xactor/man/run.md +99 -0
  85. package/python/xactor/man/sandbox.md +144 -0
  86. package/python/xactor/man/session_fork.md +74 -0
  87. package/python/xactor/man/session_harness.md +109 -0
  88. package/python/xactor/man/session_join.md +102 -0
  89. package/python/xactor/man/session_land.md +73 -0
  90. package/python/xactor/man/session_nudge.md +73 -0
  91. package/python/xactor/man/session_rest.md +73 -0
  92. package/python/xactor/man/session_scope.md +84 -0
  93. package/python/xactor/man/session_terminate.md +58 -0
  94. package/python/xactor/man/session_token.md +78 -0
  95. package/python/xactor/man/sessions.md +93 -0
  96. package/python/xactor/man/set_harness.md +15 -0
  97. package/python/xactor/man/spawn_swarm.md +80 -0
  98. package/python/xactor/man/submit_evidence.md +54 -0
  99. package/python/xactor/man/swarm_brief.md +30 -0
  100. package/python/xactor/man/task_status.md +82 -0
  101. package/python/xactor/man/tasks.md +47 -0
  102. package/python/xactor/man/todo_write.md +104 -0
  103. package/python/xactor/man/update_card.md +70 -0
  104. package/python/xactor/man/use_profile.md +90 -0
  105. package/python/xactor/man/use_widget.md +82 -0
  106. package/python/xactor/man/wait_for_task.md +105 -0
  107. package/python/xactor/man/widget-model.md +284 -0
  108. package/python/xactor/man/widgets.md +59 -0
  109. package/python/xactor/man/write_file.md +76 -0
  110. package/python/xactor/man/writing-a-widget.md +142 -0
  111. package/python/xactor/man/xylon.md +120 -0
  112. package/python/xactor/man/xylon_blame.md +80 -0
  113. package/python/xactor/man/xylon_read.md +131 -0
  114. package/python/xactor/man/xylon_search.md +125 -0
  115. package/python/xactor/manual.py +195 -0
  116. package/python/xactor/parent_watch.py +115 -0
  117. package/python/xactor/plan_mode.py +350 -0
  118. package/python/xactor/sandbox.py +856 -0
  119. package/python/xactor/seals.py +438 -0
  120. package/python/xactor/shell_hints.py +210 -0
  121. package/python/xactor/states/__init__.py +13 -0
  122. package/python/xactor/states/base.py +523 -0
  123. package/python/xactor/states/registry.py +274 -0
  124. package/python/xactor/stdlib_api.py +6345 -0
  125. package/python/xactor/widgets/__init__.py +1 -0
  126. package/python/xactor/widgets/github_tokens.py +314 -0
  127. package/python/xactor/widgets/google_aistudio.py +365 -0
  128. package/settings-schema.json +403 -0
  129. package/viewer/index.css +160 -0
  130. package/viewer/index.js +15 -0
  131. package/wgx.mjs +290659 -0
@@ -0,0 +1,214 @@
1
+ <!-- also: await_click, next, ask -->
2
+ <!-- group: Talking to the user -->
3
+
4
+ # api.await_user(target=None, timeout_seconds=300) -> dict
5
+
6
+ Wait for the person to do something, and say what they did.
7
+
8
+ **One wait, five answers**, because somebody being guided has more than one
9
+ thing they might do:
10
+
11
+ ```python
12
+ {"clicked": True} # they clicked the control you rang
13
+ {"advanced": True} # they pressed Next
14
+ {"reported": True} # they marked what is wrong and sent it
15
+ {"asked": "why this one?"} # they typed a question
16
+ {"stopped": True} # they ended it — Finish, or the chat
17
+ {"clicked": False, "reason": …} # nothing, for the whole window
18
+ ```
19
+
20
+ **`reported` is the one to read differently.** It always carries
21
+ `annotations`, and it means they stopped to say something is wrong rather than
22
+ doing what the step asked. Writing step N+1 on it is the mistake it exists to
23
+ prevent: deal with what is marked first. It ends the *step*, not the demo — you
24
+ may well want to acknowledge it with an `instruct` and carry on, and that is
25
+ your call to make rather than theirs to have made for them.
26
+
27
+ Any of them may also carry `annotations`: boxes somebody drew on the app with
28
+ **Mark what is wrong**, each with a note and a picture of the screen as it was
29
+ when they wrote it.
30
+
31
+ Pressing it opens a marking layer over the whole window, with a card of its own
32
+ in the corner — the notes taken so far, a ✕ to take any of them back, and
33
+ **Submit annotations**, which is what answers `reported`. While it is open the
34
+ step card puts its own controls away: Next beside a Done marking that could not
35
+ be reached was the reported confusion, and there is now exactly one way forward
36
+ on screen at a time. **Discard and return** leaves without sending, and the wait
37
+ does not time out while somebody is writing.
38
+
39
+ **That is the point of showing somebody something rather than asking them.** A
40
+ demo walked end to end can still be a demo of six broken screens, and until
41
+ there was somewhere to put that, the only thing a step could be answered with
42
+ was Next. Deal with what they marked before you claim anything works.
43
+
44
+ ```python
45
+ [{"region": {"x": 0.04, "y": 0.31, "width": 0.47, "height": 0.11},
46
+ "note": "this column is empty",
47
+ "$image": {...}}] # regions are 0-1, so a resize cannot move them
48
+ ```
49
+
50
+ Notes are banked the moment they are written, each with its own picture —
51
+ a tour is a sequence of screens, so one photograph taken at the end would show
52
+ the last one for every note. Drawing does not end the step it is about.
53
+
54
+ ## The row in the chat
55
+
56
+ A tour runs in its own window, so for as long as one is running it also raises a
57
+ row in the chat and in **Needs you** — otherwise an agent blocked on somebody
58
+ looks exactly like an agent that is busy, and a demo waits in a window nobody
59
+ knows to look at.
60
+
61
+ `instruct` opens that row and moves it along; `instruct(None)` closes it.
62
+
63
+ That row carries exactly two buttons, and it is worth knowing which, because
64
+ you will be tempted to tell somebody to press one that is not there.
65
+
66
+ - **Show me the app** raises the window the demo is drawn in. It answers
67
+ nothing — it is where you send somebody who has lost the tour behind the chat,
68
+ and it is the sentence `unraised` is asking you to pass on.
69
+ - **Stop** settles this wait with `{"stopped": True}` and takes the card and the
70
+ ring down.
71
+
72
+ There is no Done, no Decline and no Ask on a tour row. Finishing a demo means
73
+ *performing* it, which happens in the app, so those are not answers a row in the
74
+ chat is in a position to give — it used to draw them anyway and they silently
75
+ did nothing. Ask lives on the card instead, beside the step it is about, which
76
+ is the only place a question can be asked with the app in front of you.
77
+
78
+ Whichever surface they reach for wins — you never get two answers to the same
79
+ step.
80
+
81
+ **Finish demo**, on the step card itself — its own line under a rule, a whole
82
+ row away from Next — reports `stopped` the same way. It is there because the
83
+ card is drawn in the application's own window, and until it was wired the only
84
+ way to stop a demo was somewhere else, so "I have seen enough" meant leaving the
85
+ thing being shown.
86
+
87
+ `stopped` is not a failure. Take the card down, say what you were about to show
88
+ in a sentence, and stop.
89
+
90
+ **Except when nobody stopped it.** The row retires itself after an hour with no
91
+ step, and that arrives here as `stopped` too — carrying a `reason` that says so.
92
+ Every step restarts that clock, so it does not fire under a demo being walked
93
+ through; when it does fire, it means the demo went an hour without moving. Read
94
+ the `reason` before you thank anybody for their time:
95
+
96
+ ```python
97
+ {"stopped": True, "reason": "the demo row expired — an hour passed with no step…"}
98
+ ```
99
+
100
+ ## Three answers that are not "they did nothing"
101
+
102
+ A `{"clicked": false}` has three quite different causes and the `reason` says
103
+ which. Only the last is the person's doing.
104
+
105
+ - **`unwatched`** — *"nothing was watching that control, so a click could not
106
+ have been seen"*. The target could not be found when the wait was armed, so
107
+ the card was drawn and Next and Ask worked, but a press on the control itself
108
+ was never going to arrive. Not a person ignoring you; a step you aimed at
109
+ something that is not there.
110
+ - **the window was never in front** — see below.
111
+ - **`"observation timed out"`** — nothing was seen. Named for the *observation*
112
+ rather than the person on purpose: the harness can miss somebody who acted,
113
+ and "timed out" reads as being ignored. If presses were seen elsewhere on the
114
+ page the sentence says how many, and that is the case where they were doing
115
+ something and this did not agree it counted — not a verdict on the feature.
116
+
117
+ ## Two fields that change what an answer means
118
+
119
+ Either can ride on any of the shapes above, and both are things you cannot see
120
+ from where you are.
121
+
122
+ **`lost`** — the app navigated and the control this step is about is gone, so
123
+ the card is now describing something that is not there. Read `{"advanced": true,
124
+ "lost": …}` as "they pressed Next on a step about a screen that has moved on",
125
+ not as progress: writing the next step against a page you think is still up is
126
+ how a demo carries on over a dead app. Look before you write the next one.
127
+
128
+ **`reason: "timed out, and the app window was never in front…"`** — nobody
129
+ ignored you; the card was drawn somewhere they could not see. Tell them to click
130
+ **Show me the app** on the row in the chat, then instruct again. A bare
131
+ `"timed out"` is the other case, and only that one means they saw it and did
132
+ something else.
133
+
134
+ A press never reaches this side in a real window — the app handles it — so it is
135
+ reported out of the page and tested the way a mirrored click is: both ends
136
+ inside the control, barely any travel between them, so a press that slides off
137
+ stays the cancelled click it looks like.
138
+
139
+ ## The question box
140
+
141
+ The card carries a text input, and it is the **only channel in this API that the
142
+ person starts**. Everything else answers a question of *yours*: `ask_user` waits
143
+ for it, and the `request_*` handoffs file it and leave. None of them is a way
144
+ for somebody to raise something you did not ask about.
145
+
146
+ It is on the card rather than in the chat for two reasons, both structural. A
147
+ tour runs in its own window, so reaching the chat means leaving the thing being
148
+ shown. And you are parked inside this call: a chat message could not reach you
149
+ until the tour moved on anyway, so the moment somebody most wants to ask is the
150
+ moment the chat cannot deliver.
151
+
152
+ A question typed while nothing is listening is **held in the page** and arrives
153
+ on your next `await_user`, so it is never lost between steps.
154
+
155
+ ## Options
156
+
157
+ - **`target`** — the control this step is about. **Optional**: omit it for a step
158
+ with nothing to click ("read this, then carry on"), and Next and the question
159
+ box are still there.
160
+ - **`timeout_seconds`** — default 300. Each call is its own wait with its own
161
+ timer, so a conversation of any length costs nothing: loop, and every exchange
162
+ starts a fresh one.
163
+
164
+ ## Returns
165
+
166
+ See above. **The timeout is not an error.** Somebody who did something else is
167
+ not a failure, and noticing it is how you say something useful next.
168
+
169
+ ## Examples
170
+
171
+ The whole loop:
172
+
173
+ ```python
174
+ while True:
175
+ r = await api.await_user(target)
176
+ if notes := r.get("annotations"):
177
+ await fix_or_acknowledge(notes) # before anything else
178
+ if r.get("reported"):
179
+ await api.instruct("Got that — I'll look at it.", title="noted")
180
+ break
181
+ if r.get("clicked") or r.get("advanced"):
182
+ break
183
+ if q := r.get("asked"):
184
+ await api.reply(answer_for(q)) # the step stays; loop and wait again
185
+ elif not r.get("clicked"):
186
+ await api.instruct("Still here whenever you are.", target)
187
+ break
188
+ ```
189
+
190
+ `reported` is checked before the two that mean progress, and never falls through
191
+ to the last branch — telling somebody who has just marked three broken screens
192
+ "still here whenever you are" is the reply that makes a demo feel unread.
193
+
194
+ The `asked` branch loops rather than breaking, and answers with `reply` rather
195
+ than `instruct`. Both halves are the same point: a question is a word about the
196
+ step somebody is on, not a reason to take it away from them. `instruct` there
197
+ would move the ring and replace the sentence being asked about, which is the one
198
+ thing the question was not asking for. See `reply`.
199
+
200
+ ## Getting it wrong
201
+
202
+ **Treating a question as an interruption.** It is the tour working. Answer it,
203
+ and go wherever it points — you are a guide, not a script.
204
+
205
+ **Ignoring `{"clicked": False}`.** They did something else, or walked away. Say
206
+ something rather than posting the next step over silence.
207
+
208
+ **Calling it without an instruction up.** Next and the question box appear only
209
+ while this is waiting, so a wait with no card is a card nobody can see arriving
210
+ with controls nobody asked for.
211
+
212
+ ## See also
213
+
214
+ `instruct`, `highlight`, `ask_user`, `attach_app`
@@ -0,0 +1,142 @@
1
+ # api.bash(command, timeout=120, background=False, input=None) -> dict
2
+ <!-- group: Coding -->
3
+ <!-- also: bash_output, bash_list, bash_kill, background -->
4
+
5
+ Run a shell command. Gated. **Never raises — read `exit_code`.**
6
+
7
+ The escape hatch from the sandbox. The command itself runs in an ordinary
8
+ process with no audit hook, because the user approved *that specific command*.
9
+
10
+ ## Options
11
+
12
+ - **`command`** — a shell command line. Pipes, redirects, `&&`, globs and
13
+ `$VAR` all work.
14
+ - **`timeout`** — seconds before a foreground command is killed. Default 120.
15
+ - **`background`** — leave the command running and return immediately. Its stdout
16
+ and stderr go to a log file rather than this call's pipes; use `bash_output`,
17
+ `bash_list`, and `bash_kill` to manage it.
18
+ - **`input`** — `str` or `bytes` written to the command's stdin, which is then
19
+ closed. `api.bash("python -", input=src)` runs `src` without quoting it.
20
+
21
+ **Not every command wants a shell.** For one program with fixed arguments,
22
+ `api.run(argv)` takes them as a list and nothing is quoted or expanded. For
23
+ PowerShell, `api.powershell(script)`: inside `bash`, `$_` and `$name` are
24
+ bash's variables and are gone before PowerShell runs.
25
+
26
+ It runs with the shell's cwd as its working directory (`api.cd` moves it), and
27
+ with **stdin closed** unless you pass `input`. A command that waits for input
28
+ will not hang — it gets EOF immediately.
29
+
30
+ The command is started in its own process group, so a timeout or a user
31
+ cancellation kills the whole tree rather than orphaning children. Background
32
+ commands stay alive after the call returns, but belong to this shell: ending the
33
+ session kills their complete process trees too, rather than leaving a dev server
34
+ or watcher running without an agent that can reach it.
35
+
36
+ ## Background commands
37
+
38
+ `background=True` is for a server, watcher, or other work that must survive the
39
+ call that starts it. It returns before the command prints anything:
40
+
41
+ ```python
42
+ server = await api.bash("npm run dev", background=True)
43
+ # {"background_id": "bg1", "pid": ..., "log": ..., "running": True}
44
+ ```
45
+
46
+ Read only what arrived since your previous read with `await api.bash_output("bg1")`.
47
+ It returns `{ "output", "running", "exit_code" }`; `exit_code` is `None` while
48
+ it is still running. `await api.bash_list()` shows every background command,
49
+ including finished ones. `await api.bash_kill("bg1")` stops the command and
50
+ anything it spawned, and is safe to repeat after it has already exited.
51
+
52
+ Do not add `&` yourself in this mode. `background=True` is what replaces this
53
+ call's pipes with a log file; shell backgrounding alone still lets descendants
54
+ hold those pipes open and makes the caller wait until timeout.
55
+
56
+ On Windows this uses an MSYS bash (Git for Windows): POSIX syntax, and paths
57
+ like `/c/Users/...` and `/tmp` inside the command. Every `api.*` path accepts
58
+ those same spellings, so a directory `pwd` printed can go straight to `api.cd`.
59
+ Windows paths with backslashes need quoting in bash — unquoted, `cd C:\Users\x`
60
+ arrives as `cd C:Usersx` — so prefer `C:/Users/x`. If no bash is found the
61
+ command runs under `cmd.exe` and a warning is prepended to `stderr` — treat
62
+ that warning as a failure, because quoting, `$VAR`, globs and `;` all mean
63
+ something different there.
64
+
65
+ ## Returns
66
+
67
+ Always a dict, never an exception:
68
+
69
+ ```python
70
+ {"stdout": str, "stderr": str, "exit_code": int}
71
+ ```
72
+
73
+ `exit_code` is the process's real exit status, **except** for two cases that
74
+ both report `-1`:
75
+
76
+ | `exit_code` | `stderr` | What happened |
77
+ |---|---|---|
78
+ | `0` | | Success. |
79
+ | `> 0` | | The command failed on its own terms. |
80
+ | `-1` | `Failed to start shell: ...` | No shell could be launched. |
81
+ | `-1` | `Command timed out after Ns` | Killed at the timeout. **`stdout` is empty** — everything the command printed is discarded. |
82
+
83
+ A user cancellation is different again: it propagates as a cancellation of the
84
+ whole exec rather than returning.
85
+
86
+ ## Examples
87
+
88
+ Check the exit code, always:
89
+
90
+ ```python
91
+ r = await api.bash("pnpm build")
92
+ if r["exit_code"] != 0:
93
+ return {"failed": True, "stderr": r["stderr"][-3000:]}
94
+ return "build ok"
95
+ ```
96
+
97
+ A long run needs both timeouts raised — this call's, and the exec's own
98
+ `timeout_seconds` on the tool call, or the shell is killed out from under it:
99
+
100
+ ```python
101
+ r = await api.bash("pnpm test", timeout=900)
102
+ return r["stdout"][-4000:]
103
+ ```
104
+
105
+ Getting raw file text the line-numbered `read_file` cannot give you:
106
+
107
+ ```python
108
+ return (await api.bash("sed -n '40,60p' src/index.ts"))["stdout"]
109
+ ```
110
+
111
+ ## Getting it wrong
112
+
113
+ **Ignoring `exit_code`.** `bash` does not raise on a failed command. A build
114
+ that failed and a build that passed both come back as a dict; only
115
+ `exit_code` distinguishes them.
116
+
117
+ **Reading `stdout` after a timeout.** It is empty. If a command may exceed its
118
+ timeout, tee its output to a file and read that afterwards.
119
+
120
+ **Confusing the two `-1`s.** "Failed to start" and "timed out" share an exit
121
+ code and are only distinguishable from `stderr`.
122
+
123
+ **Editing files through `bash`.** `sed -i`, `>` redirects and `git checkout`
124
+ all work, but **bash changes are not journalled**. They do not appear in
125
+ `xylon_get_diff`, and turn-scoped revert cannot undo them. Use `write_file` and
126
+ `edit_file` for file contents and keep `bash` for builds, tests and git.
127
+
128
+ **Running an interactive command.** stdin is `/dev/null`. `git rebase -i`,
129
+ `npm init` without `-y`, and anything that prompts will fail or do nothing.
130
+
131
+ **Assuming bash on Windows.** Check `stderr` for the "no MSYS bash found"
132
+ warning before believing an `exit_code: 0` from a command that used bash
133
+ syntax.
134
+
135
+ **Nesting another interpreter in the command.** `powershell -Command "…"`,
136
+ `python -c "…"` and `node -e "…"` put a second language inside bash's quoting,
137
+ inside your Python string's escaping. Use `api.powershell(script)` or
138
+ `api.run([...], input=script)`, which have no quoting to get wrong.
139
+
140
+ ## See also
141
+
142
+ `run`, `powershell`, `sandbox`, `permissions`, `write_file`, `edit_file`, `xylon`
@@ -0,0 +1,137 @@
1
+ # Handing the browser to a smaller model
2
+ <!-- also: driver, browser-session, driving-model -->
3
+ <!-- group: Working on a website -->
4
+
5
+ Driving a website is a long tail of small, mechanical decisions. Put them on a
6
+ cheap model in its own session and talk to it over the relay.
7
+
8
+ ## Why
9
+
10
+ Two costs, and they compound.
11
+
12
+ **Context.** Page structure accumulates. Every `outline()`, every `search()`,
13
+ every batch report lands in the context of whoever called it. Twenty steps into
14
+ a console flow, a session that started out reasoning about *your* task is mostly
15
+ full of button lists. The work the user asked for gets worse as the browsing
16
+ gets longer.
17
+
18
+ **Latency.** "Which of these two buttons did they mean" is not a hard question.
19
+ Answering it on a frontier model, forty times, is most of the wall-clock in a
20
+ long flow — and it is decision latency, not page latency, so none of the Layer 0
21
+ speed work touches it.
22
+
23
+ A child session on a small model absorbs both. It holds the page context; you
24
+ hold the task.
25
+
26
+ ## The shape
27
+
28
+ ```python
29
+ # 1. A child session, on a small model, in this repo.
30
+ child = await api.agent(
31
+ prompt=(
32
+ "You are driving a browser for me. Use api.use_widget / api.drive to "
33
+ "reach a site, then api.outline / api.search / api.find to locate things "
34
+ "and the action verbs to act. Load whole chains and await api.drain(); "
35
+ "do not step one action at a time, and do not take screenshots. "
36
+ "Relay me a short summary after each objective — what you did and what "
37
+ "the page says now. Ask me before anything destructive or paid."
38
+ ),
39
+ model="claude-haiku-4-5", # or any small tier api.man('sessions') lists
40
+ )
41
+ node = child["session_id"]
42
+
43
+ # 2. Open a channel and give it work.
44
+ await api.relay_connect(node)
45
+ await api.relay_send(node, "Create a Vercel token named ci-deploy, no expiry. "
46
+ "Tell me the token's name and expiry, never its value.")
47
+
48
+ # 3. Get on with something else, then read what came back.
49
+ inbox = await api.relay_inbox()
50
+ ```
51
+
52
+ `agent` takes any model your providers expose — `api.man('sessions')`
53
+ covers the lifecycle, and the model list is whatever is configured. Haiku is the
54
+ obvious Claude choice; a small GPT tier works the same way. The point is the
55
+ tier, not the vendor.
56
+
57
+ ## Where the boundary goes
58
+
59
+ Give the driver the **objective**, not the steps:
60
+
61
+ > "Create a Vercel token named ci-deploy with no expiry."
62
+
63
+ not
64
+
65
+ > "Click Settings, then Tokens, then the Create button, then…"
66
+
67
+ The second one is you doing the driving through a slower channel. If you already
68
+ know the exact steps, that is a widget — write one (`writing-a-widget`) and stop
69
+ paying a model to replay it.
70
+
71
+ Ask for **findings**, not transcripts. A driver that relays every batch report
72
+ has moved the context problem, not solved it. "Token created, expires never,
73
+ value extracted to `VERCEL_TOKEN`" is the whole useful content of forty actions.
74
+
75
+ ## Choosing the tool
76
+
77
+ | | |
78
+ |---|---|
79
+ | `delegate(prompt, model)` | **One question, one answer.** Blocks, returns a string, no session, no relay. Right for "go read this page and tell me the price". Wrong for anything with state. |
80
+ | `agent(prompt, wait=False, model=...)` | **A driver that lives.** Its own session, its own browser, its own context. Relay to it as the task unfolds. This is the pattern here. |
81
+ | `session_fork` | An independent clone. Use when the child should not share your repo state. |
82
+
83
+ `session_join` waits for a child's current turn if you would rather block than
84
+ poll the inbox.
85
+
86
+ ## Keep it out of *its* loop too
87
+
88
+ Everything in `widget-model` about efficient navigation applies to the driver,
89
+ and matters more there because it is making more of the decisions. The prompt
90
+ above says the important parts explicitly, and it is worth keeping them:
91
+
92
+ - **load chains, `drain()` once** — not one awaited action at a time
93
+ - **`outline()` before anything**, to orient cheaply
94
+ - **no screenshots** — a picture costs a round trip through the model, which is
95
+ the expensive kind; `right_of="Name"` costs a round trip through the page
96
+ - **scope every lookup** with `under=`
97
+
98
+ A small model told to "look at the page and decide" will screenshot. Tell it not
99
+ to.
100
+
101
+ ## Permissions still belong to the user
102
+
103
+ A child session is not a way around a prompt. `use_widget`, `drive`,
104
+ `browser_exec` and `browser_screenshot` are gated in the child exactly as they
105
+ are in you, and the card names the delegate that asked. That is deliberate: the
106
+ user is approving *the driver's* action, and should be able to see it was the
107
+ driver.
108
+
109
+ A driver running unattended needs `session_harness` and explicit grants — see
110
+ `harness`. It will never be prompted, so anything not granted is refused with a
111
+ message it can read.
112
+
113
+ ## Getting it wrong
114
+
115
+ **Relaying every step.** You have paid for a second session and kept all the
116
+ context anyway. Ask for outcomes.
117
+
118
+ **Sending the steps instead of the objective.** If you know the steps, the answer
119
+ is a widget, not a driver.
120
+
121
+ **Letting it screenshot.** It will if you do not say otherwise, and it is the
122
+ single most expensive habit available to it.
123
+
124
+ **Expecting a secret back.** Use `extract_secret`; the value is written to the
125
+ workspace's `.env` file (not to any running process's environment) and never
126
+ crosses a model boundary in either direction. A shell does not see it as
127
+ `$NAME` until it loads the file: `api.bash('set -a; . ./.env; set +a; curl ... -H "Authorization: Bearer $NAME"')`
128
+ tests a credential without printing it.
129
+
130
+ **A driver with no widget on a site you drive often.** The driver rediscovers the
131
+ site every session. Have it write the widget once and use that instead.
132
+
133
+ ## See also
134
+
135
+ `sessions`, `agent`, `relay`, `agent`,
136
+ `agent-vs-fork`, `widget-model`, `writing-a-widget`, `finding`,
137
+ `harness`, `permissions`
@@ -0,0 +1,53 @@
1
+ # What the person did in your tabs
2
+ <!-- also: user-activity, browser-notice, typed, clicked -->
3
+ <!-- group: Working on a website -->
4
+
5
+ You are told when the person works in one of your browser tabs. There is no
6
+ call to make: it arrives on its own, once, at the end of your next tool result,
7
+ or ahead of your next prompt if you were idle. It does not wake you.
8
+
9
+ ```
10
+ ──
11
+ Browser: the person worked in your tabs.
12
+ tab "Stripe – API keys":
13
+ clicked "Create secret key"
14
+ typed into "Key name": "staging"
15
+ tab "Sign in":
16
+ typed into "Password" (hidden field, text not shown)
17
+ Read the page again before acting on it. Nothing is waiting on you …
18
+ ```
19
+
20
+ ## What is reported
21
+
22
+ - **Clicks** on buttons, links, tabs and menu items, by their label.
23
+ - **Typing**, as each field's final value. Typing in one field, however long, is
24
+ one line.
25
+ - **Choices** in selects, checkboxes and radio buttons.
26
+ - **Navigation** the person caused: a link followed, an address typed.
27
+ - **Tabs** the person switched to or closed.
28
+
29
+ Your own actions are never reported back to you.
30
+
31
+ ## What stays hidden
32
+
33
+ Text in a field the site hides on screen: a password field, or one styled to
34
+ mask its text. Also fields the site marks as a card number, card code, one-time
35
+ code or password. The decision is made when the field is first focused, so a
36
+ "show password" button does not reveal it later. You are told only that
37
+ something was typed there.
38
+
39
+ Fields inside another site's embedded frame (a payment form, for example) are
40
+ not watched at all.
41
+
42
+ ## Getting it wrong
43
+
44
+ **Treating it as an instruction.** The person clicking "Delete" is not them
45
+ asking you to delete something. It tells you the page has changed. Read it
46
+ again before your next action on it.
47
+
48
+ **Trusting the page you read earlier.** If anything is listed for a tab, what
49
+ you saw on it before is out of date.
50
+
51
+ ## See also
52
+
53
+ `drive`, `browser_tabs`, `outline`
@@ -0,0 +1,84 @@
1
+ # api.browser_attach(target_session_id, timeout=None) -> dict
2
+ <!-- also: attach, borrow-browser, browser-ownership -->
3
+ <!-- group: Working on a website -->
4
+
5
+ Borrow another session's browser, waiting if someone else is using it.
6
+
7
+ Every browser belongs to the session that opened it. No other session can reach
8
+ it by accident — that is what stops two sessions driving one tab and navigating
9
+ it out from under each other. This call is the deliberate exception.
10
+
11
+ ## Why ownership exists
12
+
13
+ Concurrent sessions used to share whatever tab was open. Nothing errored: each
14
+ one attached to a page it did not open, and each reported confident conclusions
15
+ about a site it was not actually looking at. A session mapping Supabase decided
16
+ the site was behind a login wall while reading another session's Netlify tab.
17
+
18
+ So a tab now has an owner, and `api.drive` / `api.use_widget` will open a new
19
+ page rather than adopt a claimed one. If everything open is claimed and you have
20
+ no browser yourself, you get an error telling you so — not somebody else's page.
21
+
22
+ ## Options
23
+
24
+ - **`target_session_id`** — the session whose browser you want. Not a
25
+ dispatcher id, and not your own session.
26
+ - **`timeout`** — milliseconds to wait for it. Default 120,000.
27
+
28
+ ## Returns
29
+
30
+ ```python
31
+ {"status": "attached", "dispatcher_id": str, "owner": str,
32
+ "url": str, "title": str, "shell_status": str}
33
+ ```
34
+
35
+ If the wait runs out you get `{"error": ..., "occupied": True}` instead, naming
36
+ the holder and how long they have had it. **It does not raise**, so check for
37
+ `occupied` rather than assuming you have the browser.
38
+
39
+ While you hold it, your `browser_exec` runs against the borrowed page.
40
+
41
+ ## Examples
42
+
43
+ Look at what a driver child ended up on:
44
+
45
+ ```python
46
+ r = await api.browser_attach(child_session_id)
47
+ if r.get("occupied"):
48
+ return f"Could not borrow it: {r['error']}"
49
+ try:
50
+ out = await api.browser_exec(None, "print(await api.outline())")
51
+ print(out["output"])
52
+ finally:
53
+ await api.browser_detach(child_session_id)
54
+ ```
55
+
56
+ Wait a long time for a slow flow to finish, rather than failing fast:
57
+
58
+ ```python
59
+ r = await api.browser_attach("session-42", timeout=600_000)
60
+ ```
61
+
62
+ ## Getting it wrong
63
+
64
+ **Forgetting `browser_detach`.** The lease is held until you release it. The
65
+ owner and every other waiter is blocked the whole time. Use `try/finally`.
66
+
67
+ **Treating the result as success.** A timed-out attach returns a dict with
68
+ `occupied: True`. Running `browser_exec` after one drives *your own* browser, or
69
+ fails — not the one you meant to borrow.
70
+
71
+ **Attaching to get around ownership.** If you just need a page, open your own
72
+ with `api.drive` or `api.use_widget`. Borrowing is for looking at a browser that
73
+ is already somewhere you need it to be.
74
+
75
+ **Passing a dispatcher id.** It takes a session id (`'session-42'`). The
76
+ dispatcher is derived from it.
77
+
78
+ **Expecting it to release when your turn ends.** It does not. A session that
79
+ attaches and stops without detaching leaves the browser held.
80
+
81
+ ## See also
82
+
83
+ `browser_detach`, `browser_exec`, `drive`, `use_widget`, `sessions`,
84
+ `browser-driver`, `browser_plumbing`
@@ -0,0 +1,50 @@
1
+ # api.browser_close_tab(tab_id) -> dict
2
+ <!-- also: close-tab -->
3
+ <!-- group: Working on a website -->
4
+
5
+ Close one of your session's tabs.
6
+
7
+ Closing the tab you were driving moves you to another of your tabs when there is
8
+ one, so you are never left driving a dead page.
9
+
10
+ ## Options
11
+
12
+ - **`tab_id`** — from `api.browser_tabs()`.
13
+
14
+ ## Returns
15
+
16
+ ```python
17
+ {"status": "closed", "tab_id": str,
18
+ "focused_tab_id": str | None, "remaining": int}
19
+ ```
20
+
21
+ `focused_tab_id` is what you are driving now. `None` with `remaining: 0` means
22
+ your window is empty — the next `api.drive` or `api.use_widget` starts a new
23
+ one.
24
+
25
+ ## Examples
26
+
27
+ Tidy up everything you opened for a job:
28
+
29
+ ```python
30
+ for t in (await api.browser_tabs())["tabs"]:
31
+ if t["url"].startswith("https://docs."):
32
+ await api.browser_close_tab(t["tab_id"])
33
+ ```
34
+
35
+ ## Getting it wrong
36
+
37
+ **Closing a tab the user opened themselves.** Only tabs in your window are
38
+ addressable, so you cannot — but do not close tabs you did not open *for this
39
+ task* either, if the session has been doing other work.
40
+
41
+ **Closing the last tab and then calling `browser_exec`.** With nothing left,
42
+ there is no page to run against. Check `remaining`.
43
+
44
+ **Treating it as a way to stop a flow.** A half-finished action on a console is
45
+ half-finished whether or not you close the tab. Clean up what you created on the
46
+ site first.
47
+
48
+ ## See also
49
+
50
+ `browser_tabs`, `browser_open_tab`, `browser_focus_tab`, `browser_exec`