agent: clarify script primitives and save behavior

Updates documentation to clarify that script primitives use CSS
selectors instead of backendNodeIds. Explains how to use `/nodeDetails`
to get selectors. Clarifies the difference in `/save` behavior
between `--no-llm` and LLM modes.
This commit is contained in:
Adrià Arrufat committed 2026-06-05 11:26:24 +02:00
1 parent d18297f204
commit 157aa30bdc
3 files changed
+66 -25

No files matched your search

+31 -14
View File
@@ -110,24 +110,36 @@ Only recorded browser primitives are installed globally:
| Primitive | Arguments | Runs in |
|-----------|-----------|---------|
| `goto` | `goto(url)` or `goto({ url, timeout, waitUntil })` | Browser session |
| `goto` | `goto(url[, { timeout, waitUntil }])` | Browser session |
| `extract` | `extract(schema)` or `extract({ schema })` | Browser page via extractor; returns a JS object or array |
| `evaluate` | `evaluate(script)` or `evaluate({ script, url, timeout, waitUntil, save })` | Browser page JS context |
| `click` | `click({ selector })` or `click({ backendNodeId })` | Browser page |
| `fill` | `fill({ selector, value })` or `fill({ backendNodeId, value })` | Browser page |
| `scroll` | `scroll()` or `scroll({ x, y, backendNodeId })` | Browser page |
| `waitForSelector` | `waitForSelector(selector)` or `waitForSelector({ selector, timeout })` | Browser page |
| `waitForScript` | `waitForScript(script)` or `waitForScript({ script, timeout })` | Browser page JS context |
| `hover` | `hover({ selector })` or `hover({ backendNodeId })` | Browser page |
| `press` | `press({ key })` or `press({ key, selector, backendNodeId })` | Browser page |
| `selectOption` | `selectOption({ selector, value })` or `selectOption({ backendNodeId, value })` | Browser page |
| `setChecked` | `setChecked({ selector, checked })` or `setChecked({ backendNodeId, checked })` | Browser page |
| `evaluate` | `evaluate(script[, { url, timeout, waitUntil, save }])` | Browser page JS context |
| `click` | `click({ selector })` | Browser page |
| `fill` | `fill({ selector, value })` | Browser page |
| `scroll` | `scroll()` or `scroll({ x, y })` | Browser page |
| `waitForSelector` | `waitForSelector(selector[, { timeout }])` | Browser page |
| `waitForScript` | `waitForScript(script[, { timeout }])` | Browser page JS context |
| `hover` | `hover({ selector })` | Browser page |
| `press` | `press({ key })` or `press({ key, selector })` | Browser page |
| `selectOption` | `selectOption({ selector, value })` | Browser page |
| `setChecked` | `setChecked({ selector, checked })` | Browser page |
`waitUntil` accepts `"load"`, `"domcontentloaded"`, `"networkidle"`, or
`"done"`.
Prefer CSS selectors in saved scripts. `backendNodeId` values are tied to the
current DOM snapshot and are not stable after navigation or DOM mutation.
The `[, { … }]` is an optional trailing options object: leading arguments are
positional (`waitForSelector("#row", { timeout: 2000 })`), and the options ride
in a final object. Passing a single object with everything
(`waitForSelector({ selector: "#row", timeout: 2000 })`) is equivalent — that's
the shape `/save` records into saved scripts. An option can't be a bare
positional, though: `waitForSelector("#row", 2000)` is an error.
Script primitives address elements by CSS selector only. The tools that hand
out `backendNodeId`s (`tree`, `findElement`, `nodeDetails`) aren't installed in
the script context, and a raw node ID wouldn't survive replay anyway. When
you're exploring in the REPL and have a `backendNodeId` — e.g. the leading
number on a `/tree` line, or a `/findElement` hit — run `/nodeDetails
backendNodeId=<id>` to get a durable CSS `selector`, then paste that into your
script.
## Navigation
@@ -299,7 +311,12 @@ Only replayable browser actions are recorded:
and `extract`.
- Not recorded: read-only exploration tools such as `tree`, `markdown`, `html`,
`links`, `findElement`, `consoleLogs`, `getUrl`, `getCookies`, and `getEnv`.
- Natural-language prompts and recording comments are written as `//` comments.
- Natural-language prompts are written as `//` comments above the actions they
produced.
This is the deterministic `--no-llm` transcription. When `/save` runs with an
LLM it instead synthesizes an idiomatic script from the session and is asked to
emit JavaScript only, so those `//` comments generally won't appear.
## Error Handling
+24 -3
View File
@@ -194,6 +194,23 @@ replayable. Always
synthesize a CSS selector from the attributes (`id`, `class`,
`name`, `action`, `tag_name`) and use that.
You don't have to hand-roll the selector. Every node in the `/tree`
output carries a `backendNodeId` (the leading number on each line), and
`/nodeDetails` turns one into a durable selector for you:
```
> /nodeDetails backendNodeId=15
```
It returns a ready-to-use CSS `selector` that resolves to that node
(plus its tag, id, class, name, and attributes) — the canonical way to
go from a `/tree` (or `/findElement`) hit to a selector you can paste
into `/click` or `/fill`. We reached for `/detectForms` above because
the two login/signup forms share field names, so a form-scoped selector
(keyed on `action`) is cleaner here — but `/nodeDetails` is the quickest
path whenever you have a single `backendNodeId` and just want its
selector without guessing.
Now fill the form:
```
@@ -320,9 +337,13 @@ goto("https://news.ycombinator.com");
extract({ topStories: [{ selector: ".athing", fields: { rank: ".rank", title: ".titleline > a", url: { selector: ".titleline > a", attr: "href" } } }] });
```
Natural-language REPL turns are not saved as executable JavaScript.
When a natural-language turn produces recorded browser actions, the
prompt is kept as a `//` comment above those actions.
Natural-language REPL turns are never saved as executable JavaScript.
In the deterministic `--no-llm` transcription, the prompt that produced
a set of recorded actions is kept as a `//` comment above them, so the
script stays readable. The LLM `/save` path is different: it rewrites
the whole session into an idiomatic script and is told to emit
JavaScript only, so it generally drops those comments rather than
preserving them verbatim.
## 5. Running deterministically
+11 -8
View File
@@ -235,14 +235,17 @@ is origin-scoped and persists across navigations within a session).
From the REPL, `/save [file.js]` writes the session back to a `.js` file
and `/load <path>` runs a script from disk against the current session.
State-mutating commands (`/goto`, `/click`, `/fill`, `/scroll`, `/hover`,
`/selectOption`, `/setChecked`, `/waitForSelector`, `/press`, `/evaluate`,
`/extract`) are saved; read-only commands (`/tree`, `/markdown`,
`/links`, `/findElement`, …) and the natural-language turns that produced
them are not. Natural-language turns are saved as `// <prompt>` comments
above the resulting JavaScript calls so the script stays readable. In the
basic REPL (`--no-llm`) `/save` transcribes the session deterministically;
with an LLM it synthesizes an equivalent idiomatic script.
`/save` works one of two ways. **With `--no-llm`** it transcribes the session
deterministically: state-mutating commands (`/goto`, `/click`, `/fill`,
`/scroll`, `/hover`, `/selectOption`, `/setChecked`, `/waitForSelector`,
`/waitForScript`, `/press`, `/evaluate`, `/extract`) become JavaScript calls,
read-only commands (`/tree`, `/markdown`, `/links`, `/findElement`, …) are
dropped, and each natural-language prompt that produced recorded actions is
written as a `// <prompt>` comment above those calls so the script stays
readable. **With an LLM** it instead synthesizes an idiomatic script from the
whole session — the synthesis prompt asks for JavaScript only ("no
commentary"), so the result generally has no such comments: the model folds
intent into the code and drops dead-ends.
### JavaScript Script Running