@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.
- package/LICENSE.txt +49 -0
- package/README.md +33 -2
- package/ThirdPartyNotices.txt +4685 -0
- package/package.json +30 -4
- package/provider-clis.json +1211 -0
- package/python/XACTOR.md +146 -0
- package/python/browser_actor_host.py +456 -0
- package/python/cdp_page.py +363 -0
- package/python/harness_check.py +77 -0
- package/python/requirements.txt +1 -0
- package/python/test_line_endings.py +185 -0
- package/python/xactor/__init__.py +18 -0
- package/python/xactor/__main__.py +226 -0
- package/python/xactor/api.py +882 -0
- package/python/xactor/bridge.py +784 -0
- package/python/xactor/dom.py +117 -0
- package/python/xactor/man/add_card.md +89 -0
- package/python/xactor/man/agent-vs-fork.md +108 -0
- package/python/xactor/man/agent.md +333 -0
- package/python/xactor/man/ask_model.md +110 -0
- package/python/xactor/man/ask_reference.md +56 -0
- package/python/xactor/man/ask_user.md +103 -0
- package/python/xactor/man/attach_app.md +595 -0
- package/python/xactor/man/attach_ide.md +65 -0
- package/python/xactor/man/attach_ide_surface.md +31 -0
- package/python/xactor/man/await_user.md +214 -0
- package/python/xactor/man/bash.md +142 -0
- package/python/xactor/man/browser-driver.md +137 -0
- package/python/xactor/man/browser_activity.md +53 -0
- package/python/xactor/man/browser_attach.md +84 -0
- package/python/xactor/man/browser_close_tab.md +50 -0
- package/python/xactor/man/browser_detach.md +50 -0
- package/python/xactor/man/browser_exec.md +66 -0
- package/python/xactor/man/browser_focus_tab.md +47 -0
- package/python/xactor/man/browser_hybrid.md +47 -0
- package/python/xactor/man/browser_open.md +50 -0
- package/python/xactor/man/browser_open_tab.md +58 -0
- package/python/xactor/man/browser_plumbing.md +107 -0
- package/python/xactor/man/browser_screenshot.md +56 -0
- package/python/xactor/man/browser_snapshot.md +52 -0
- package/python/xactor/man/browser_tabs.md +67 -0
- package/python/xactor/man/bulletin.md +88 -0
- package/python/xactor/man/capture_page.md +54 -0
- package/python/xactor/man/cd.md +64 -0
- package/python/xactor/man/chess_move.md +53 -0
- package/python/xactor/man/children.md +89 -0
- package/python/xactor/man/close_task.md +14 -0
- package/python/xactor/man/compact.md +75 -0
- package/python/xactor/man/drive.md +140 -0
- package/python/xactor/man/drive_space.md +72 -0
- package/python/xactor/man/driving.md +168 -0
- package/python/xactor/man/edit_file.md +112 -0
- package/python/xactor/man/finding.md +194 -0
- package/python/xactor/man/fork_agent.md +45 -0
- package/python/xactor/man/glob.md +83 -0
- package/python/xactor/man/grep.md +115 -0
- package/python/xactor/man/harness.md +165 -0
- package/python/xactor/man/harness_check.md +70 -0
- package/python/xactor/man/harness_format.md +257 -0
- package/python/xactor/man/highlight.md +66 -0
- package/python/xactor/man/instruct.md +111 -0
- package/python/xactor/man/ls.md +63 -0
- package/python/xactor/man/man.md +86 -0
- package/python/xactor/man/merge_wiz.md +40 -0
- package/python/xactor/man/notebook.md +98 -0
- package/python/xactor/man/overview.md +89 -0
- package/python/xactor/man/permissions.md +137 -0
- package/python/xactor/man/phase.md +150 -0
- package/python/xactor/man/powershell.md +74 -0
- package/python/xactor/man/profiles.md +60 -0
- package/python/xactor/man/project_drive.md +49 -0
- package/python/xactor/man/read_bulletin.md +89 -0
- package/python/xactor/man/read_file.md +90 -0
- package/python/xactor/man/relay.md +216 -0
- package/python/xactor/man/reply.md +72 -0
- package/python/xactor/man/reply_message.md +78 -0
- package/python/xactor/man/report_bug.md +118 -0
- package/python/xactor/man/request_answer.md +142 -0
- package/python/xactor/man/request_merge_decisions.md +121 -0
- package/python/xactor/man/request_operator_action.md +152 -0
- package/python/xactor/man/request_review.md +122 -0
- package/python/xactor/man/request_user_action.md +126 -0
- package/python/xactor/man/return_card.md +71 -0
- package/python/xactor/man/run.md +99 -0
- package/python/xactor/man/sandbox.md +144 -0
- package/python/xactor/man/session_fork.md +74 -0
- package/python/xactor/man/session_harness.md +109 -0
- package/python/xactor/man/session_join.md +102 -0
- package/python/xactor/man/session_land.md +73 -0
- package/python/xactor/man/session_nudge.md +73 -0
- package/python/xactor/man/session_rest.md +73 -0
- package/python/xactor/man/session_scope.md +84 -0
- package/python/xactor/man/session_terminate.md +58 -0
- package/python/xactor/man/session_token.md +78 -0
- package/python/xactor/man/sessions.md +93 -0
- package/python/xactor/man/set_harness.md +15 -0
- package/python/xactor/man/spawn_swarm.md +80 -0
- package/python/xactor/man/submit_evidence.md +54 -0
- package/python/xactor/man/swarm_brief.md +30 -0
- package/python/xactor/man/task_status.md +82 -0
- package/python/xactor/man/tasks.md +47 -0
- package/python/xactor/man/todo_write.md +104 -0
- package/python/xactor/man/update_card.md +70 -0
- package/python/xactor/man/use_profile.md +90 -0
- package/python/xactor/man/use_widget.md +82 -0
- package/python/xactor/man/wait_for_task.md +105 -0
- package/python/xactor/man/widget-model.md +284 -0
- package/python/xactor/man/widgets.md +59 -0
- package/python/xactor/man/write_file.md +76 -0
- package/python/xactor/man/writing-a-widget.md +142 -0
- package/python/xactor/man/xylon.md +120 -0
- package/python/xactor/man/xylon_blame.md +80 -0
- package/python/xactor/man/xylon_read.md +131 -0
- package/python/xactor/man/xylon_search.md +125 -0
- package/python/xactor/manual.py +195 -0
- package/python/xactor/parent_watch.py +115 -0
- package/python/xactor/plan_mode.py +350 -0
- package/python/xactor/sandbox.py +856 -0
- package/python/xactor/seals.py +438 -0
- package/python/xactor/shell_hints.py +210 -0
- package/python/xactor/states/__init__.py +13 -0
- package/python/xactor/states/base.py +523 -0
- package/python/xactor/states/registry.py +274 -0
- package/python/xactor/stdlib_api.py +6345 -0
- package/python/xactor/widgets/__init__.py +1 -0
- package/python/xactor/widgets/github_tokens.py +314 -0
- package/python/xactor/widgets/google_aistudio.py +365 -0
- package/settings-schema.json +403 -0
- package/viewer/index.css +160 -0
- package/viewer/index.js +15 -0
- 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`
|