effect-browser 0.2.0-beta.9 → 0.3.0-beta.10
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/README.md +849 -1161
- package/dist/Browser-S4kvpmqa.mjs +605 -0
- package/dist/Browser-S4kvpmqa.mjs.map +1 -0
- package/dist/Browser.d.mts +127 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +211 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +297 -0
- package/dist/BrowserEvent.mjs.map +1 -0
- package/dist/Cdp.d.mts +32 -0
- package/dist/Cdp.mjs +61 -0
- package/dist/Cdp.mjs.map +1 -0
- package/dist/Change-DNvztNLu.mjs +138 -0
- package/dist/Change-DNvztNLu.mjs.map +1 -0
- package/dist/Change-iMlcuzVD.d.mts +71 -0
- package/dist/Change.d.mts +2 -0
- package/dist/Change.mjs +2 -0
- package/dist/Chromium.d.mts +29 -81
- package/dist/Chromium.mjs +57 -392
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect.d.mts +62 -0
- package/dist/Expect.mjs +114 -0
- package/dist/Expect.mjs.map +1 -0
- package/dist/Frame-BERmf_gM.d.mts +117 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +123 -0
- package/dist/Frame.mjs.map +1 -0
- package/dist/Moment.d.mts +2 -0
- package/dist/Moment.mjs +288 -0
- package/dist/Moment.mjs.map +1 -0
- package/dist/Motion.d.mts +40 -0
- package/dist/Motion.mjs +122 -0
- package/dist/Motion.mjs.map +1 -0
- package/dist/Page-B5cjaUp5.mjs +618 -0
- package/dist/Page-B5cjaUp5.mjs.map +1 -0
- package/dist/Page-BlPjfibv.d.mts +595 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-BI8XGvDu.d.mts +209 -0
- package/dist/Plan-D1wrJVs8.mjs +508 -0
- package/dist/Plan-D1wrJVs8.mjs.map +1 -0
- package/dist/Plan.d.mts +2 -3
- package/dist/Plan.mjs +2 -2
- package/dist/Policy.d.mts +76 -0
- package/dist/Policy.mjs +237 -0
- package/dist/Policy.mjs.map +1 -0
- package/dist/Presentation.d.mts +48 -0
- package/dist/Presentation.mjs +117 -0
- package/dist/Presentation.mjs.map +1 -0
- package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
- package/dist/Snapshot.d.mts +2 -0
- package/dist/Snapshot.mjs +79 -0
- package/dist/Snapshot.mjs.map +1 -0
- package/dist/Stage.d.mts +64 -0
- package/dist/Stage.mjs +237 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
- package/dist/Supervisor-TNKNOVzu.mjs +599 -0
- package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-B6xr8nVi.mjs +261 -0
- package/dist/context-B6xr8nVi.mjs.map +1 -0
- package/dist/index.d.mts +17 -14
- package/dist/index.mjs +18 -13
- package/dist/match.inpage-xOe9LNO0.mjs +48 -0
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
- package/dist/page-B8tUQELg.mjs +5073 -0
- package/dist/page-B8tUQELg.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-Cd04Okid.mjs +264 -0
- package/dist/url-Cd04Okid.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +666 -0
- package/src/BrowserError.ts +234 -0
- package/src/BrowserEvent.ts +296 -0
- package/src/Cdp.ts +101 -0
- package/src/Change.ts +88 -0
- package/src/Chromium.ts +102 -0
- package/src/Expect.ts +180 -0
- package/src/Frame.ts +130 -0
- package/src/Moment.ts +479 -0
- package/src/Motion.ts +166 -0
- package/src/Page.ts +522 -0
- package/src/Plan.ts +663 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +467 -0
- package/src/Supervisor.ts +599 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +610 -0
- package/src/internal/input/dispatch.ts +161 -0
- package/src/internal/input/edit.inpage.ts +162 -0
- package/src/internal/input/evidence.inpage.ts +164 -0
- package/src/internal/input/gate.ts +224 -0
- package/src/internal/input/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +184 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +376 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +195 -0
- package/src/internal/input/settle.ts +50 -0
- package/src/internal/input/style.ts +186 -0
- package/src/internal/input/targets.inpage.ts +293 -0
- package/src/internal/input/targets.ts +148 -0
- package/src/internal/page/bridge.ts +386 -0
- package/src/internal/page/context.ts +312 -0
- package/src/internal/page/lane.ts +311 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +165 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +776 -0
- package/src/internal/pictures/clock.ts +141 -0
- package/src/internal/pictures/counts.ts +92 -0
- package/src/internal/pictures/pictures.ts +520 -0
- package/src/internal/pictures/transport.ts +242 -0
- package/src/internal/reading/choose.ts +100 -0
- package/src/internal/reading/context.inpage.ts +201 -0
- package/src/internal/reading/controls.inpage.ts +158 -0
- package/src/internal/reading/expect.inpage.ts +70 -0
- package/src/internal/reading/match.inpage.ts +92 -0
- package/src/internal/reading/names.inpage.ts +306 -0
- package/src/internal/reading/outline.inpage.ts +260 -0
- package/src/internal/reading/reading.ts +223 -0
- package/src/internal/reading/ready.inpage.ts +164 -0
- package/src/internal/reading/ready.ts +111 -0
- package/src/internal/reading/subjects.inpage.ts +188 -0
- package/src/internal/reading/text.inpage.ts +135 -0
- package/src/internal/reading/walk.inpage.ts +220 -0
- package/src/internal/supervisor/lifecycle.ts +416 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +94 -0
- package/src/internal/timeline/fold.inpage.ts +123 -0
- package/src/internal/timeline/history.inpage.ts +229 -0
- package/src/internal/timeline/marks.inpage.ts +207 -0
- package/src/internal/timeline/record.inpage.ts +133 -0
- package/src/internal/timeline/sight.inpage.ts +122 -0
- package/src/internal/timeline/window.ts +96 -0
- package/src/internal/timeline/words.ts +65 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
- package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
- package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
- package/dist/Bootstrap.d.mts +0 -3
- package/dist/Bootstrap.mjs +0 -2
- package/dist/Browser-BF9_sORP.d.mts +0 -388
- package/dist/Browser.mjs.map +0 -1
- package/dist/BrowserData-CPIIIHol.d.mts +0 -585
- package/dist/BrowserData.d.mts +0 -3
- package/dist/BrowserData.mjs +0 -695
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
- package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
- package/dist/BrowserRuntime.d.mts +0 -3
- package/dist/BrowserRuntime.mjs +0 -3
- package/dist/Capture.d.mts +0 -56
- package/dist/Capture.mjs +0 -678
- package/dist/Capture.mjs.map +0 -1
- package/dist/CaptureData-CYriZHX1.mjs +0 -201
- package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
- package/dist/CaptureData.d.mts +0 -219
- package/dist/CaptureData.mjs +0 -2
- package/dist/Errors.d.mts +0 -158
- package/dist/Errors.mjs +0 -207
- package/dist/Errors.mjs.map +0 -1
- package/dist/Images-yCG9BmRU.mjs +0 -76
- package/dist/Images-yCG9BmRU.mjs.map +0 -1
- package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
- package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
- package/dist/PageControl.d.mts +0 -15
- package/dist/PageControl.mjs +0 -34
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
- package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
- package/dist/Plan-Dyxu2q8C.mjs +0 -543
- package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
- package/dist/PlanData-B668RxbQ.d.mts +0 -4831
- package/dist/PlanData.d.mts +0 -3
- package/dist/PlanData.mjs +0 -268
- package/dist/PlanData.mjs.map +0 -1
- package/dist/PlanInput-BJcPPncs.mjs +0 -75
- package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
- package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
- package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
- package/dist/Testing.d.mts +0 -479
- package/dist/Testing.mjs +0 -2147
- package/dist/Testing.mjs.map +0 -1
- package/dist/Timeline-fV22Xk-S.d.mts +0 -29
- package/dist/Timeline.d.mts +0 -4
- package/dist/Timeline.mjs +0 -20
- package/dist/Timeline.mjs.map +0 -1
- package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
- package/dist/TimelineData.d.mts +0 -3
- package/dist/TimelineData.mjs +0 -482
- package/dist/TimelineData.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,1221 +1,909 @@
|
|
|
1
|
-
#
|
|
1
|
+
# effect-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser automation for [Effect](https://effect.website) agents, over Playwright: page control, a
|
|
4
|
+
compact page outline for models, screencast frames, a record of what visibly changed on a page,
|
|
5
|
+
windows over a page's events, changes and frames, and moments, an account in pictures and words of
|
|
6
|
+
what a page showed and what changed on it. An agent drives its pages through
|
|
7
|
+
[`effect-agent-browser`](../agent-browser), with Yielded Agent's browser tools.
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
A `BrowserSession<E>` is the live, host-only capability returned by the supplying implementation. It preserves callback failures and diagnostics of type `E`, one action budget, one native connection and one set of capture/page-control reservations. `implementation` identifies the control implementation. `closeChecked` performs the owner's cleanup and fails if its required cleanup was not confirmed. On concrete Chromium and Browserbase sessions it returns that same frozen cleanup receipt on success; the generic session permits discarding that value. Helpers that only use operations take `AnySession`, an alias for `BrowserSession<unknown>`. Helpers that supervise callback failures must stay generic in `E` or the concrete session so they retain those failures.
|
|
10
|
-
|
|
11
|
-
All browser operations use issued `Page` and `Frame` objects on the original connection. `Capture.start`, `Capture.stream` and `PageControl` authenticate the exact Page privately; spreading or decoding an object cannot copy authority. An absent registration fails with reason `UnregisteredSession` and outcome `undispatched`. A copy, fabricated value or separately loaded runtime can cause that refusal; it does not establish which occurred. A registered Page with page control disabled still fails `Unsupported`. Tools and adapters bind the issued Page together with its original session for callback supervision and checked owner cleanup. Neither opens another browser.
|
|
12
|
-
|
|
13
|
-
Mutations on one Page share its permit with its Frames; healthy Pages can proceed independently. Registry operations remain serialized. An observed node remains usable only until an invalidating event; a replaced node is never searched for again. A timed-out or interrupted mutation with an unacknowledged native command has an `unknown` outcome and is never automatically replayed. Its exact page is revoked before bounded closure. Positive closure reports `containment: PageClosed` and preserves healthy pages; unconfirmed closure fences the session and reports `SessionFenced`. A page that its own popup/dialog policy paused is kept open for the operator instead, and its unknown work reports `PagePaused`. Containment does not make the original action known. `performed` means dispatched input was acknowledged before a later step failed; it also must not be replayed blindly. `undispatched`, `rejected`, `performed` and `unknown` remain distinct outcomes.
|
|
14
|
-
|
|
15
|
-
## Self-managed Chromium
|
|
16
|
-
|
|
17
|
-
`effect-browser/chromium` provides `Chromium`. It uses the same modeled browser owner, native driver, exact-node observations, bootstrap bindings, capture and page control as a hosted session. It requires no Browserbase client, project, key, allocation response or provider endpoint. `BrowserSession<E>` is their shared modeled capability; `BrowserbaseSession<E>` retains the separate provider reference, artifacts, Live View, handoff and release contracts.
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
import { NodeServices } from "@effect/platform-node";
|
|
21
|
-
import { Effect, Layer } from "effect";
|
|
22
|
-
import * as Browser from "effect-browser/browser";
|
|
23
|
-
import { BrowserPolicy } from "effect-browser/browser-data";
|
|
24
|
-
import { Chromium } from "effect-browser/chromium";
|
|
25
|
-
|
|
26
|
-
const program = Browser.scoped(Chromium.launch(BrowserPolicy.unrestricted()), (browser) =>
|
|
27
|
-
Effect.gen(function* () {
|
|
28
|
-
yield* browser.initialPage.navigate({ url: "https://example.com" });
|
|
29
|
-
return yield* browser.initialPage.observe({ scope: "viewport" });
|
|
30
|
-
}),
|
|
31
|
-
).pipe(
|
|
32
|
-
Effect.provide(
|
|
33
|
-
Chromium.layer({
|
|
34
|
-
viewport: { width: 1280, height: 720 },
|
|
35
|
-
pageControl: true,
|
|
36
|
-
launch: { headless: true, chromiumSandbox: true },
|
|
37
|
-
}).pipe(Layer.provide(NodeServices.layer)),
|
|
38
|
-
),
|
|
39
|
-
);
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The Layer requires Effect's `Crypto`, which the host platform supplies: `NodeServices.layer` here, `BunServices.layer` on Bun, or `NodeCrypto.layer` alone. Process references, connection ids, native binding names and handoff tokens are drawn from it. The Layer captures it once, so `launch`, `acquire` and `attach` never require it. Layer construction validates configuration and starts nothing. The optional `playwright-core` peer is loaded only when needed. `Chromium.acquire(policy, { bootstrap })` starts one owned Chromium process and registers cleanup before waiting for a connection; its cached `connect` yields one `ChromiumSession<E>`. `Chromium.launch` combines those steps. These static operations access the configured service, as do `Chromium.attach` and the corresponding `BrowserbaseBrowser` operations. Bootstrap consumer errors and services remain in the acquisition signatures.
|
|
43
|
-
|
|
44
|
-
`Browser.scoped(open, use)` is the common workflow supervisor for both sources, including borrowed attachment. It runs the acquisition once in its own scope, retains the concrete browser type in `use`, and races workflow completion against typed fail-session callbacks. The workflow has its own child scope: fibers and finalizers finish before `closeChecked` runs, so callback cleanup may still use a healthy browser. Checked closure runs on success, failure, thrown defects and cancellation. A body failure and a cleanup failure both remain in this workflow's own final Effect cause; neither is overwritten. An outer race or timeout can select another result and discard that losing cause even though cleanup ran. Configure the provider's `onCleanup` to record its receipt in a host-owned sink outside the raced workflow when cleanup evidence must survive that composition. A body that completes concurrently with a fail-session callback can still win the race; supervision does not establish failure priority. The ownership finalizer still runs if acquisition itself fails. No failed acquisition or action is replayed.
|
|
45
|
-
|
|
46
|
-
The same combinator supports `open.pipe(Browser.scoped(use))`, including a reusable `const use = Browser.scoped((browser) => browser.initialPage.observe())` stored before choosing the provider. That unannotated callback sees common browser operations; use an annotated callback or the data-first form for provider-specific members or typed binding diagnostics. The returned function infers the supplying session's callback error independently of its acquisition error. Explicit generic applications must follow the curried overload's revised parameter lists: four outer parameters (`S, A, E2, R2`) and three returned parameters (`E, AE, AR`). It removes the scopes it owns while preserving other required services and error types. Returning a browser, stream or other live capability from `use` does not extend that resource's lifetime. The old provider-specific `withBrowser` methods and `BrowserRuntime.withBrowser` are replaced by this one public operation.
|
|
47
|
-
|
|
48
|
-
### A long-lived session in a Layer
|
|
49
|
-
|
|
50
|
-
An application can share one acquired session through a Layer for its finite application scope:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
import { NodeServices } from "@effect/platform-node";
|
|
54
|
-
import { Context, Effect, Layer } from "effect";
|
|
55
|
-
import { Chromium, type ChromiumSession } from "effect-browser/chromium";
|
|
56
|
-
import { BrowserPolicy } from "effect-browser/browser-data";
|
|
57
|
-
|
|
58
|
-
class SharedBrowser extends Context.Service<SharedBrowser, ChromiumSession>()(
|
|
59
|
-
"app/SharedBrowser",
|
|
60
|
-
) {}
|
|
61
|
-
|
|
62
|
-
const shared = Layer.effect(
|
|
63
|
-
SharedBrowser,
|
|
64
|
-
Chromium.launch(BrowserPolicy.unrestricted({ maxElapsedMillis: 300_000 })),
|
|
65
|
-
).pipe(
|
|
66
|
-
Layer.provide(Chromium.layer({ onCleanup: recordReceipt })),
|
|
67
|
-
Layer.provide(NodeServices.layer),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const program = Effect.gen(function* () {
|
|
71
|
-
const browser = yield* SharedBrowser;
|
|
72
|
-
return yield* browser.initialPage.observe({ scope: "viewport" });
|
|
73
|
-
}).pipe(Effect.provide(shared));
|
|
9
|
+
```sh
|
|
10
|
+
npm install effect-browser@beta effect playwright-core
|
|
11
|
+
npx playwright-core install chromium
|
|
74
12
|
```
|
|
75
13
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
14
|
+
`Chromium.layer` launches that Chromium without its sandbox, as Playwright does, so an exploit in a
|
|
15
|
+
page's renderer runs with your user's privileges. `Chromium.layer({ sandbox: true })` runs it
|
|
16
|
+
sandboxed. Hosts that don't allow unprivileged user namespaces, such as many containers and Ubuntu
|
|
17
|
+
23.10 or later by default, can't start the sandbox; opening then fails and says so.
|
|
18
|
+
|
|
19
|
+
`Chromium.layer` leaves signals to your program: Playwright's own handlers, which close every
|
|
20
|
+
browser and, on SIGINT, exit before any finalizer runs, are off. Run the program with
|
|
21
|
+
`NodeRuntime.runMain` from `@effect/platform-node`, which interrupts it on SIGINT and SIGTERM, so
|
|
22
|
+
the browser closes with its scope. A signal that ends the process outright still takes Chromium
|
|
23
|
+
with it, since its pipe closes, but leaves Playwright's temporary profile behind.
|
|
24
|
+
|
|
25
|
+
| Module | What it holds |
|
|
26
|
+
| -------------- | --------------------------------------------------------------------------------- |
|
|
27
|
+
| `Browser` | The `Browser` service: tabs, recent events and the Playwright context |
|
|
28
|
+
| `Chromium` | A local Chromium as a `Browser` layer |
|
|
29
|
+
| `Cdp` | Any DevTools endpoint as a `Browser` layer |
|
|
30
|
+
| `Page` | One tab: navigation, reads, screenshots, input, waits and the screencast |
|
|
31
|
+
| `Snapshot` | The model-readable outline of a page, with refs for its controls |
|
|
32
|
+
| `Frame` | A screencast frame |
|
|
33
|
+
| `BrowserEvent` | Tabs, documents, loads, actions, dialogs, pointer motion and the browser's end |
|
|
34
|
+
| `Motion` | A bounded pointer planner, as a value, and the tuned sigma-lognormal one |
|
|
35
|
+
| `Presentation` | Input performed for viewers: a presenter, and its views of pages |
|
|
36
|
+
| `Stage` | The source of a live output, switched between pages and stamped |
|
|
37
|
+
| `BrowserError` | Typed failures, whether input reached the page first, and what each leaves |
|
|
38
|
+
| `Policy` | Judges that read what an input means, and a guard that acts on them unattended |
|
|
39
|
+
| `Change` | What visibly changed on a page over a window, element by element |
|
|
40
|
+
| `Moment` | A page's events, changes and frames over a window; a moment, laid out as a prompt |
|
|
41
|
+
| `Plan` | A walk recorded from a page's events, replayed on a fresh page by subject |
|
|
42
|
+
| `Supervisor` | A browser kept open across losses and session ends, as generations |
|
|
43
|
+
|
|
44
|
+
A snapshot outlines the viewport, or the whole page, with a ref such as `e12` on each control, and
|
|
45
|
+
lists those controls as values, `Snapshot.Control`: each one's ref, kind, name and what it holds,
|
|
46
|
+
a select's options, and whether it is disabled, checked or takes typed text. `within` reads only
|
|
47
|
+
inside the elements a CSS selector matches, and `query` keeps the lines that hold some text.
|
|
48
|
+
`Page.waitFor` waits, in one call the page answers as it comes, until what a selector matches
|
|
49
|
+
shows, hides or is enabled, with some text if asked. `press` takes `on`, a ref it focuses before the
|
|
50
|
+
first key, such as a link only the keyboard reaches. `Change.describe` tells a change in the words
|
|
51
|
+
a moment uses. The actions a call performs record a caller's id as their `correlation` when it runs
|
|
52
|
+
under `Page.correlate(id)`, such as a model's tool call.
|
|
53
|
+
|
|
54
|
+
`Page.zoom` captures a region in viewport CSS pixels as a `Zoom` schema value with `region` and `image`;
|
|
55
|
+
crop pixel coordinates need the region's origin added before using them as click coordinates.
|
|
56
|
+
|
|
57
|
+
`Page.click` returns a `ResolvedTarget` captured before input: the requested point, element label,
|
|
58
|
+
role, accessible name, context, cursor and link target. Pixel targeting resolves through the page
|
|
59
|
+
script and keeps the original point; the receipt names the control even when a nested child
|
|
60
|
+
received the hit. Refs inside same-origin frames are measured, scrolled and checked for cover in the
|
|
61
|
+
top viewport. A ref that no longer names an element of the current documents, including one in a
|
|
62
|
+
frame that has since navigated, fails with `StaleRef` naming that ref, with or without a guard.
|
|
63
|
+
|
|
64
|
+
A subject names an element durably: its role, accessible name and tag, and its context, the words
|
|
65
|
+
around it that say which one it is. In a table, `row` is the row's header, or else its first cell
|
|
66
|
+
with letters, so a rank or a price never names it, and `column` is the header over it, spanned cells
|
|
67
|
+
counted; elsewhere, `label` is the words just before it in its row, item, group or block. `heading`
|
|
68
|
+
is the nearest heading above it, left out for what is pinned to the viewport. The page binds each to
|
|
69
|
+
the element, so a value is never read under another column's header.
|
|
70
|
+
|
|
71
|
+
`Page.find` reads structure without the outline, in one call to the page. It returns every element
|
|
72
|
+
that matches a query, in tree order, each a `Found`: a ref the actions take, its `subject`, its box,
|
|
73
|
+
whether it is in the viewport, and its state (disabled, focused, and checked, expanded, selected,
|
|
74
|
+
pressed or a heading's level where they apply). A `role` matches ignoring case. A `name` matches
|
|
75
|
+
when it reads the same once spaces are collapsed and case folded, or when a `RegExp` finds itself
|
|
76
|
+
in it. `text` matches the smallest element showing it, and a control or heading for the words
|
|
77
|
+
inside it; `near` matches whole words of the context, as replay's choice does. With no rule,
|
|
78
|
+
`find` returns every element in scope that has a role or is a control. There is no ordinal: a
|
|
79
|
+
caller tells equal elements apart by their context, and finding none is an empty result, not a
|
|
80
|
+
failure. `scope: "document"` reads the whole page; the default is the viewport.
|
|
81
|
+
|
|
82
|
+
`Page.text` reads what the viewport shows, or one element whole with `scope` set to its ref, in one
|
|
83
|
+
call to the page: a line per block, table cells apart by tabs, cut at a line after `maxChars`
|
|
84
|
+
(12,000 by default). Like the outline, it leaves out what the page hides from assistive technology,
|
|
85
|
+
such as icon glyphs. What a field holds reads `••••` unless `unmask` is set, and a secret field's
|
|
86
|
+
always does: a password, a one-time code or a card field, one whose style shows dots for what it
|
|
87
|
+
holds, as a PIN field's may, and one that was secret when the library saw it, such as a password
|
|
88
|
+
its page now reveals. The outline shows what fields hold, as the agent needs, but masks a secret
|
|
89
|
+
field the same way.
|
|
90
|
+
|
|
91
|
+
Reading the viewport, the outline, `find` and `text` skip a subtree whose box lies outside it before
|
|
92
|
+
styling anything in it, so a long table costs only its rows in view. A box says nothing of what is
|
|
93
|
+
positioned out of it, so a subtree is kept when its box is empty; when it holds what is painted at
|
|
94
|
+
the viewport's edges, corners or middle, beneath any transparent layer too, or in its top layer; or
|
|
95
|
+
when it holds what its style attribute pins in view. Something a stylesheet pins inside a subtree
|
|
96
|
+
out of view, away from those points or ignoring the pointer, is missed. The outline's `above` and
|
|
97
|
+
`below` count the parts skipped, each an element out of view with all it holds.
|
|
98
|
+
|
|
99
|
+
`Page.ready` waits until a page is ready to be shown, in one call that checks in the page until it
|
|
100
|
+
is, or until the time is up: its document is parsed and has painted since, nothing that ends is
|
|
101
|
+
animating in view, its fonts and the images in view have loaded, nothing in view is marked busy
|
|
102
|
+
(`aria-busy`), and the viewport shows something: text, a canvas drawn on, a picture or drawing
|
|
103
|
+
larger than an icon, a video or a frame. A canvas mounted blank, as chart libraries mount theirs,
|
|
104
|
+
or a spinner alone is still loading. A screen that only says "Loading…" in words reads as ready,
|
|
105
|
+
and a WebGL canvas drawn once, without keeping its drawing, reads as blank. A hidden tab, which
|
|
106
|
+
paints nothing, is never ready.
|
|
107
|
+
|
|
108
|
+
With `quietMillis`, the screen must then also stay still that long: no screencast frame comes,
|
|
109
|
+
counted from the first frame of a capture the wait starts itself, since a hosted browser sends that
|
|
110
|
+
frame over half a second late. A capture's silence alone proves little: a stalled connection holds
|
|
111
|
+
frames on their way, and Chromium sends frames only while few acknowledgements are unanswered. So
|
|
112
|
+
the spell counts only while every acknowledgement is answered, and it ends with one more call to the
|
|
113
|
+
page, whose answer arrives behind every frame sent before it; a frame that comes first starts the
|
|
114
|
+
spell again. That call costs the wait one round trip. A capture on a connection of its own, as on
|
|
115
|
+
Browserbase, gets one round trip there too, beside the call, since frames there do not wait behind
|
|
116
|
+
the page's calls: a 4 fps canvas whose capture connection stalled for a frame read as still in 2
|
|
117
|
+
of 2 waits before, and in none since. Where the page's changes are recorded, the
|
|
118
|
+
page's own check also waits until nothing in view has changed for the spell, so frames the browser
|
|
119
|
+
holds back, as an encoder's backlog does, cannot pass for a still page. Stillness is a heuristic
|
|
120
|
+
on a canvas: a canvas that keeps drawing, such as a live chart, is never still, and one that pauses
|
|
121
|
+
longer than `quietMillis` between phases reads as still. On the slot machine fixture, with the
|
|
122
|
+
connection stalled for 450 to 900 ms toward either end while the reels spun, none of 132 waits
|
|
123
|
+
ended early, where all 36 did before.
|
|
124
|
+
|
|
125
|
+
`Page.ready({ still: "content", quietMillis })` instead observes a full interval without visible
|
|
126
|
+
recorded DOM changes. It starts the recorder itself and starts the interval again in each new
|
|
127
|
+
document; navigation retries share the original timeout. Canvas animation can continue while DOM
|
|
128
|
+
content is ready. This does not prove pixel or screencast stillness and inherits the change
|
|
129
|
+
recorder's limits for canvas, SVG, frames, shadow content and values changed only by scripts.
|
|
130
|
+
|
|
131
|
+
`Page.changes({ since, until })` says what visibly changed on a page over a window, in one call to
|
|
132
|
+
the page, element by element, as a `Change`: text that changed, appeared, disappeared or came and
|
|
133
|
+
went (`brief`), a field's value, or the title. Each says what it showed at the window's start and
|
|
134
|
+
end, how often that changed, the lowest and highest of a number that changed more than once, when
|
|
135
|
+
it last changed before the window, so news stands apart from what keeps changing, and its
|
|
136
|
+
`subject` with the context that binds it: a price under its row and column. A field's value reads
|
|
137
|
+
`••••` unless `unmask` is set, and a secret field's always does. A change names its `cause`, the
|
|
138
|
+
trusted input it followed, only where it was that input's doing. It must not have been in flux: in
|
|
139
|
+
the 10 s before the input it had not changed, and nothing had come into or left what it sits in, so
|
|
140
|
+
a ticker's next tick or a feed's next line names no click. Then it changed within 3 s inside what
|
|
141
|
+
the input acted on or its row, form, dialog or controlled element, so "Order placed" a server's
|
|
142
|
+
reply later keeps its click; or elsewhere within 500 ms, on a page where nothing had changed in the
|
|
143
|
+
2 s before the input, so a menu a portal puts at the end of the page keeps its click, and a chat
|
|
144
|
+
line beside a ticking board does not. What holds the input's target, as a menu holds its item, does
|
|
145
|
+
not count against it. Against inert clicks on boards whose 2 to 20 cells tick every 0.25 to 6 s,
|
|
146
|
+
the rule credited none of 401 ticks and none of 96 chat lines, where a second's quiet on the element
|
|
147
|
+
and 500 ms anywhere had credited about half the ticks of the slower boards and every chat line.
|
|
148
|
+
What a busy page costs is the credit for an effect out of a click's reach, which a moment then
|
|
149
|
+
tells as a step; on a still page, an unrelated line 250 ms after a click is still credited.
|
|
150
|
+
|
|
151
|
+
A page records once something reads its changes: the first read starts it, and finds nothing yet.
|
|
152
|
+
From then the page's own session starts the recorder at the start of each later document, once it
|
|
153
|
+
is parsed, until nobody has read it for two minutes; a page nobody reads records nothing and costs
|
|
154
|
+
nothing. The first read also registers the recorder, in the same round trip, and every later read
|
|
155
|
+
is one call. A browser whose clock no capture has mapped yet maps it on its first read of changes,
|
|
156
|
+
once, in four round trips more. While it runs, a MutationObserver marks what changed: a text-only
|
|
157
|
+
element, as most prices are, is read at once, and anything else, with whether it was in view, as
|
|
158
|
+
the page renders it, through an IntersectionObserver that forces no layout. A change counts as seen
|
|
159
|
+
when it was in the viewport as the page rendered it, its style showing it, so one scrolled away
|
|
160
|
+
later is still told, and something removed or hidden only if it was in view before. On a
|
|
161
|
+
2,000-cell table rewritten every 50 ms, locally, the page's busy time went from about 100 ms per
|
|
162
|
+
3 s to about 230 recording, where an empty observer costs about 180.
|
|
163
|
+
|
|
164
|
+
The record keeps 256 elements with their last 32 changes, for a minute. On a busier page, an
|
|
165
|
+
element that keeps changing gives way first, then one never seen in view, then one that has gone,
|
|
166
|
+
so news stays; whatever gives way, or finds no room, is counted in `dropped`, and `from` moves
|
|
167
|
+
past it: the record never claims to be whole where it is not. A window can start at a previous
|
|
168
|
+
read's `Changes`, continuing exactly where it ended on the page's own clock, or at a frame's
|
|
169
|
+
paint, and end at a frame's paint, so a narrator airing a frame late is told nothing that frame
|
|
170
|
+
does not show. Times are host milliseconds through the browser's one clock mapping, as frames'
|
|
171
|
+
are. The record does not see pictures, a canvas, frames, shadow roots or SVG, a value a script
|
|
172
|
+
sets, or a class that reveals an element it has not seen before, such as a toast already on the
|
|
173
|
+
page, though its later changes it does.
|
|
174
|
+
|
|
175
|
+
A page's timeline has three tracks on the browser's host clock: its events, what changed on it,
|
|
176
|
+
and its frames. `page.window({ since, until })` reads them over a window, a `Moment.Window`: the
|
|
177
|
+
events and frames the page keeps, at no call, and its changes, in one call, the first starting its
|
|
178
|
+
record as any read of changes does. `since` is a previous window, to go on exactly where it ended, a
|
|
179
|
+
frame, a host time, or a `Duration` back from `until`; `until` is a frame or a host time, now by
|
|
180
|
+
default. A consumer that airs a frame seconds after it was painted reads the window that ends at
|
|
181
|
+
that frame, so that a line it writes now tells what its viewers will see. A part that cannot be
|
|
182
|
+
read is in `missing`, with why: a window fails only for bounds that are not finite, or a negative
|
|
183
|
+
reach back.
|
|
184
|
+
`Moment.account(window)` says what was so at the window's end. Its `changing` are the changes still
|
|
185
|
+
under way: what kept changing, more than once in the window or already before it, and changed
|
|
186
|
+
again within twice its own pace before the end, as a ticking price does. Its `settled` are the
|
|
187
|
+
rest, each showing what it settled at, as a price that ticked and stopped does. Its `stillFor` is
|
|
188
|
+
how long the page had been still, by the last change in view its record shows or the last paint
|
|
189
|
+
among its screencast frames; a screenshot shows the page, not when it changed. Frames show paint
|
|
190
|
+
only while a capture runs, and the record does not see a canvas, so a canvas that draws with no
|
|
191
|
+
capture running reads as still. A wait for a still screen is `Page.ready({ quietMillis })`, which
|
|
192
|
+
also holds through a stalled connection.
|
|
193
|
+
|
|
194
|
+
`Moment.capture` is a window that ends at a picture of the page now. It takes the picture first,
|
|
195
|
+
then the window up to its paint, from where a previous window or moment ended, so the changes hold
|
|
196
|
+
nothing the picture does not show. A picture, a read of changes or an outline that cannot be had is
|
|
197
|
+
missing from the moment, with why, and the moment is still made. `Moment.toPrompt` leads with what
|
|
198
|
+
changed: news and what settled first, then what was still changing, by the moment's account, with
|
|
199
|
+
the cells of a column that changed together on one line; a change that kept changing says whether
|
|
200
|
+
it still was at the moment or since when it had settled, so a narrator need not assert a value that
|
|
201
|
+
is still moving. It names an action as what a change followed, or as a step: where changes followed
|
|
202
|
+
it but none names it, as when its effect landed out of its reach on a busy page; where its effect is
|
|
203
|
+
drawn, such as a click on a canvas, which only the screenshots show; or where it came before the
|
|
204
|
+
record began. Hovers, scrolls and attempts that nothing followed are left out, and a failed action is told
|
|
205
|
+
only as failed: its error is advice to the caller that acted, and may name a ref. A moment whose
|
|
206
|
+
record saw none of its window, as a page's first, whose read starts the record, says that what
|
|
207
|
+
changed was not recorded and lists every step instead; one whose record began within the window
|
|
208
|
+
says nothing changed only after that. What it could not read, it names, with why, and a last frame
|
|
209
|
+
that is not the picture is not shown as the moment.
|
|
210
|
+
|
|
211
|
+
`Plan` rehearses a walk once and replays it later, near live, with no model call.
|
|
212
|
+
`Plan.fromEvents(page.recentEvents)` keeps one page's completed actions with their subjects and
|
|
213
|
+
the options they were given, its navigations with the address asked for and the one reached, and
|
|
214
|
+
what was typed as an input slot named for its field, never the text itself. A navigation to an
|
|
215
|
+
address with a credential withheld is an input slot too, `address`, so replay goes where the caller
|
|
216
|
+
says, never to the address without its credential. `Plan.replay(page,
|
|
217
|
+
plan, { inputs })` takes the steps in turn: before each that acts, it waits for `Page.ready`
|
|
218
|
+
(`settle` sets how, or `false` not to wait), finds the one element the step's subject names with
|
|
219
|
+
`Plan.locate`, and acts on it. An element that does not repeat every part of the recorded context
|
|
220
|
+
is another subject: its row read whole, so a row named "Wrapped BTC" is not the row "BTC" (laid out
|
|
221
|
+
again without rows, as cards, one of its fields must start with the row's words), its column where
|
|
222
|
+
it is under one, and its label and heading read the same or found as words in order. So the one
|
|
223
|
+
element left once the recorded one has gone, such as Alice's "Remove" after Bob's, drifts rather
|
|
224
|
+
than stands in. The rest rank by how much they read the same; a tie is `Ambiguous`, since there is
|
|
225
|
+
no ordinal. Text typed into whatever had focus went into the focused field, which the action
|
|
226
|
+
records as its subject, so replay inserts into that field wherever focus has gone, retaining its
|
|
227
|
+
existing text. Caret positions are not recorded: insertion uses the field's caret when focused.
|
|
228
|
+
A press recorded on a subject resolves and focuses it before sending keys. Text first
|
|
229
|
+
typed into a secret field is typed only into one. An element step never falls back to coordinates.
|
|
230
|
+
A step recorded at a point presses the same place within the element found, brought into view
|
|
231
|
+
first, and only when a press there reaches that element, not something over it. Replay stops at
|
|
232
|
+
the first step it cannot take, with a `ReplayError` naming the step and why: `Missing`,
|
|
233
|
+
`Ambiguous`, `Drifted` (nothing with the subject's role and name repeats its context, or the walk
|
|
234
|
+
did not arrive at the recorded site), the step's `BrowserError`, or `Uncertain`. Arrival waits
|
|
235
|
+
are bounded by `arrival` (five seconds by default), including redirects after an action. A zero
|
|
236
|
+
wait makes one read without polling; that read retains the page operation's own timeout.
|
|
237
|
+
Each `Action` now records its ending address, so a bounded event history can still record the site
|
|
238
|
+
of later actions after its earlier navigation has expired.
|
|
239
|
+
|
|
240
|
+
`Plan.take(page, plan, index)` takes one step and returns a `Receipt`: the step index, host-clock
|
|
241
|
+
`startedAt` and `at`, and `taken`, `verified` or `skipped`. `Plan.replay` returns those receipts;
|
|
242
|
+
a `ReplayError` includes completed receipts and a serializable `stopped` value with the next step
|
|
243
|
+
and whether its input is `unsent` or `unknown`. Resume with `{ from: error.stopped }`. A numeric
|
|
244
|
+
index means fresh input, so never convert an unresolved stopping point into a number to retry it.
|
|
245
|
+
An interrupted call remains interrupted. Persist that a step has begun **before** calling it;
|
|
246
|
+
a missing receipt, including after a process restart, is `Stopped({ step, input: "unknown" })`.
|
|
247
|
+
The consumer owns that durable journal and the decision to reconcile a consequential operation.
|
|
248
|
+
|
|
249
|
+
An optional `Step.expect` checks `Checked`, `Appeared`, `Gone` or `Navigated` before input and
|
|
250
|
+
waits for it afterward. `verified` means the specified observation held; it is not a transaction
|
|
251
|
+
receipt from the target service. Keep the receipt with its plan to interpret that evidence.
|
|
252
|
+
`Checked` can converge a checkbox click to a desired boolean.
|
|
253
|
+
Unknown non-repeatable input is never automatically sent again: only a positive check explicitly
|
|
254
|
+
marked `durable: true` permits continuation. This is the author's assertion of a lasting,
|
|
255
|
+
uniquely scoped read-back, such as a saved order receipt. A toast or an intermediate URL cannot
|
|
256
|
+
justify it. A negative or failed durable check still leaves the operation uncertain.
|
|
257
|
+
Navigation, hover, scroll, selection and replacement typing without submission can be repeated;
|
|
258
|
+
back, reload, insertion, presses, drags and ordinary clicks cannot. `Plan.repeatable(step)` says
|
|
259
|
+
which applies. `Plan.restore(page, plan, { upTo, inputs })` reconstructs earlier steps on a fresh
|
|
260
|
+
page using those same rules and refuses unresolved commitments. `upTo` is exclusive.
|
|
261
|
+
|
|
262
|
+
`Plan.Plan.new(steps)` selects the stored version: recordings without expected checks remain
|
|
263
|
+
version 1; checks require version 2. Both decode, with version and contents validated together.
|
|
264
|
+
Old 0.2 plans and unknown versions do not decode.
|
|
265
|
+
|
|
266
|
+
`Browser.Options.guard` is the input policy. Its `InputRequest` schema contains the action, the
|
|
267
|
+
resolved element, the page's URL and title, and the `facts` the page's structure establishes:
|
|
268
|
+
`form-submit`, `cross-origin`, `download`, `upload`, `secret` (typing with `type` into a
|
|
269
|
+
password, one-time code or card field, or submitting a form that holds a filled one), `scripted` (the activated
|
|
270
|
+
control has no effect of the browser's own, so only page script decides what happens) and
|
|
271
|
+
`opaque` (nothing names what receives the input, such as a canvas or an unlabelled icon). More
|
|
272
|
+
than one may apply. They describe what the input activates, found as the browser finds it: the
|
|
273
|
+
submit button around a painted label, an SVG link around a shape, an image-map area. Hovering and
|
|
274
|
+
scrolling activate nothing and carry none. `point` is present for literal pixel targets; a ref's
|
|
275
|
+
coordinates are resolved after approval so preparation never scrolls.
|
|
276
|
+
|
|
277
|
+
Facts never come from what an element's text says: words change meaning with context and
|
|
278
|
+
language, so a payment, a deletion or a consent is for a judge to recognise. The request carries
|
|
279
|
+
the evidence one needs: the target's name and description, its dialog, the heading before it, the
|
|
280
|
+
text beside it in its row or form, and the form's fields with their types and autocomplete tokens.
|
|
281
|
+
Fields carry whether they are filled, never their values. All of it is page text the page
|
|
282
|
+
controls, to read as evidence and never as instructions. Text typed into a `secret` field reaches
|
|
283
|
+
the guard and the recorded `Action` as `redacted`, and its key events as `Unidentified` keys;
|
|
284
|
+
`press` sends and records the keys it is given, so type secrets with `type`.
|
|
285
|
+
It covers clicks, drags, typing, keys, selection, hover, scroll and navigation, including a new
|
|
286
|
+
tab's destination.
|
|
287
|
+
|
|
288
|
+
The policy's effect succeeds to allow, fails with `PolicyDenied` to refuse, or waits for a signal
|
|
289
|
+
the consumer owns to hold. For example:
|
|
89
290
|
|
|
90
291
|
```ts
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}),
|
|
102
|
-
}),
|
|
103
|
-
),
|
|
104
|
-
);
|
|
105
|
-
// result can be None; receipt still records the completed cleanup attempt's actual facts.
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
The sink above retains one host-only receipt; it is not durable storage. A direct path can return
|
|
109
|
-
`yield* browser.closeChecked` from the callback to obtain the concrete receipt, with typed failure
|
|
110
|
-
when the owner's required cleanup was unconfirmed. Repeated checked closure and scope finalization
|
|
111
|
-
reuse the same cleanup attempt. Body failure and direct or explicitly awaited interruption retain
|
|
112
|
-
checked-close errors in that workflow's own cause; a racing parent chooses which result survives.
|
|
113
|
-
The [complete compiled example](examples/shared-session.ts) shows all three compositions.
|
|
114
|
-
|
|
115
|
-
Cleanup notification is attempted once after canonical evidence has been stored. Callback
|
|
116
|
-
construction throws, defects, self-interruption and a two-second cooperative timeout are contained.
|
|
117
|
-
Browserbase's built-in diagnostic reporter and the optional `onCleanup` sink are contained
|
|
118
|
-
independently, so a broken reporter cannot suppress the sink. A failed sink does not confirm
|
|
119
|
-
cleanup, alter the stored receipt, or retry teardown. `cleanupResult` remains the canonical local
|
|
120
|
-
record. Notification timeout relies on cooperative interruption and cannot preempt synchronous
|
|
121
|
-
host code that never yields. Receipt storage, Context-writer settlement and `closeChecked` validation
|
|
122
|
-
are outside this optional-notification boundary.
|
|
123
|
-
|
|
124
|
-
Owned launch currently supports POSIX hosts (Linux and macOS). It uses a fresh temporary profile, an ephemeral loopback debugger port and one maintained CDP connection. The launcher owns those arguments; callers cannot replace them through `args`. `executablePath` selects an installed Chromium explicitly; otherwise the pinned Playwright executable is used. Headless mode and Chromium sandboxing default to enabled. `chromiumSandbox: false` is an explicit host exception, never inferred from `CI`, root execution or a connection failure. Setting the option alone does not prove the operating system's sandbox configuration. Startup waiting is bounded by `startupTimeoutMillis` (15 seconds by default, at most 60 seconds) and the owner's remaining lifetime. Acquisition does not retry a failed launch.
|
|
125
|
-
|
|
126
|
-
For an externally owned Chromium, use `browser.attach(endpoint, { policy, target?, bootstrap? })`. `endpoint` is a `Redacted<string>` containing the exact `ws://127.0.0.1:PORT/devtools/browser/ID` or IPv6-loopback equivalent advertised by that browser. HTTP discovery URLs, non-loopback hosts, credentials, queries and fragments are refused. This validates the control endpoint's shape; it does not authenticate the host running it. A supplied `target.targetId` must identify an existing page. Without one, several candidate pages are an explicit ambiguity error. Attachment preserves the existing viewport and does not replay the layer's launch arguments or create a replacement browser. Coordinate any independent controllers yourself, especially when enabling page control.
|
|
127
|
-
|
|
128
|
-
Chromium identity is `{ provider: "chromium", id }`, identifying this ownership lifetime. It is not a Browserbase session reference, a PID or an attachment credential. `close` and `cleanupResult` retain `connection` and `process` facts separately. Owned cleanup fences operations, stops capture, disposes initialization, disconnects and terminates its process group; only observed termination permits removing the temporary profile. Failure or timeout remains in `issues` and leaves `process: "unknown"` when exit was not established. Borrowed cleanup disconnects its own client and reports `process: "not-owned"`; it never terminates the external process. Repeated close calls share one result. No local result has a provider `remote: "confirmed"` field. `onCleanup` receives these bounded, host-only facts even when connection setup fails after launch.
|
|
129
|
-
|
|
130
|
-
`launch.proxy: { server, bypass? }` forwards an existing host-operated proxy to Chromium. With a proxy, the default bypass value is `<-loopback>` so Chromium does not silently exclude loopback destinations; a different bypass is an explicit host choice. Additional reviewed native flags, such as disabling QUIC and non-proxied WebRTC UDP, can be supplied through `launch.args`. This module does not implement a proxy or qualify its transport/DNS coverage. Browser policy remains `Unrestricted`; a local endpoint, URL admission or successful local test never establishes whole-browser egress containment. Preserve and test the selected enforcing proxy independently. The Browserbase integration validates its own provider-issued endpoints separately.
|
|
131
|
-
|
|
132
|
-
Owned Chromium also watches a parent-owned debugging pipe for disconnection. The pipe carries
|
|
133
|
-
no protocol commands; Playwright continues to use the loopback endpoint. If the host exits or is
|
|
134
|
-
killed, including before `connect`, the operating system closes the pipe and Chromium exits.
|
|
135
|
-
Normal scoped cleanup still checks process-group termination and removes the profile. A killed
|
|
136
|
-
host cannot produce a cleanup receipt or remove its temporary profile. Borrowed attachments have
|
|
137
|
-
no such pipe and leave their externally owned browser running.
|
|
138
|
-
|
|
139
|
-
## Passive status and native diagnostics
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
const status = yield * session.status;
|
|
143
|
-
const diagnostics = yield * session.diagnostics;
|
|
144
|
-
// status: { phase, reason, generation, busy, unresolvedDispatch, actions: { used, maximum } }
|
|
145
|
-
// diagnostics: { records, total, dropped, truncated }
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Both reads copy host memory, acquire no browser permit, charge no action and remain available
|
|
149
|
-
while busy, faulted or closed. A snapshot is not an admission token: the next operation still
|
|
150
|
-
checks its ticket. `phase` describes admission/lifecycle, not remote cleanup confirmation. Its
|
|
151
|
-
closed vocabulary includes `faulted` for a known terminal trigger, alongside `acquiring`, `open`,
|
|
152
|
-
`paused`, `detached`, `uncertain`, `closing` and `closed`; exhaustive host matches must include the
|
|
153
|
-
new case. `reason` retains the original terminal trigger through later cleanup. `busy` reports
|
|
154
|
-
active admission or pending policy cleanup. Action-count or host-read exhaustion leaves the owner
|
|
155
|
-
open and is not a terminal failure.
|
|
156
|
-
|
|
157
|
-
`actions` is the model-reachable allowance as the owner counts it: `maximum` is the policy's
|
|
158
|
-
`maxActions` and `used` the operations admitted against it, whether they then succeeded or not.
|
|
159
|
-
Every step of `fillForm` and its submit is an action, and so is each reading an agent Tool takes
|
|
160
|
-
after its action; a form's verification, `checkpoint`, `controlFacts` and `status` itself are not.
|
|
161
|
-
A refused operation is not counted, so `used` never passes `maximum`, and at `maximum` the next
|
|
162
|
-
action fails `Limit { dimension: "actions" }` undispatched while the owner stays open. A host that
|
|
163
|
-
runs one long session reads `used` instead of counting actions itself. `BrowserPolicy.maxActions`
|
|
164
|
-
accepts 1–1,000,000 (100 by default, like the host-read allowance's bound); a long session raises it
|
|
165
|
-
together with `maxElapsedMillis`.
|
|
166
|
-
|
|
167
|
-
`unresolvedDispatch` is separate from the trigger. Idle expiry records `expired` without inventing
|
|
168
|
-
an unknown dispatch. Expiry or a fail-session callback overlapping native work retains both the
|
|
169
|
-
known trigger and unresolved control. A fence or borrowed disconnection is not acknowledgement
|
|
170
|
-
that the browser stopped. Positive owned termination evidence can retire that control indication;
|
|
171
|
-
it does not reconcile earlier business effects or change their `unknown` outcomes. The concrete
|
|
172
|
-
cleanup receipt remains the authority for process termination or provider release confirmation.
|
|
173
|
-
|
|
174
|
-
Diagnostics retain the latest 32 records, oldest first, with a closed reason, disposition,
|
|
175
|
-
connection generation and host monotonic timestamp. Counters saturate at `Number.MAX_SAFE_INTEGER`;
|
|
176
|
-
`truncated` and `dropped` disclose eviction. Records contain no URL, title, message, native object
|
|
177
|
-
or consumer cause. Typed callback errors remain in `failure` and `bindingDiagnostics`.
|
|
178
|
-
|
|
179
|
-
Under popup-close policy, a popup beyond `maxPages` is quarantined and closed once. Its originating
|
|
180
|
-
click can finish, while new browser work is refused until cleanup is confirmed. Confirmed closure
|
|
181
|
-
preserves the original page's usability. The native cleanup pool is bounded at 32 operations;
|
|
182
|
-
the two-second acknowledgement deadline does not free an unresolved native promise's slot.
|
|
183
|
-
Lost acknowledgement fences control without replay; a late acknowledgement cannot reopen it.
|
|
184
|
-
Capacity refusal before dispatch is a known block rather than fabricated uncertainty. Dialog-cap
|
|
185
|
-
overflow follows the same bounded dismissal rules. An acknowledged before-unload dismissal can
|
|
186
|
-
retire only the exact navigation captured when its dialog arrived and subsequently rejected.
|
|
187
|
-
An attributed popup/dialog pause quarantines its exact Page and stops its capture and bindings;
|
|
188
|
-
healthy peers keep working. The original Page reports `paused` and permits explicit close, while
|
|
189
|
-
input remains refused. Its page timeline ends `stale`, as an all-page pause ends every page's, so a
|
|
190
|
-
run waiting to start on it fails preparation at once. An action the pause interrupts on that page, such as the click that opened
|
|
191
|
-
the dialog, is never closed away: its unknown outcome reports `containment: PagePaused`, and the
|
|
192
|
-
quarantined page's native work does not hold up a handoff's drain. Recovery requires a drained
|
|
193
|
-
session handoff and explicit operator release, then newly acquired Page/Frame authority. Unattributed policy or shared connection failures fence
|
|
194
|
-
the session conservatively.
|
|
195
|
-
|
|
196
|
-
## Browser operations
|
|
197
|
-
|
|
198
|
-
### Page and Frame authority
|
|
199
|
-
|
|
200
|
-
`session.initialPage` is the Page acquired on the first connection. `session.listPages()` returns
|
|
201
|
-
bounded metadata; `session.page(info)` authenticates it and issues the canonical Page for that
|
|
202
|
-
native page and generation. A Page provides navigation, input, observation, exact-node actions,
|
|
203
|
-
checkpoint, readiness, viewport control and close. `page.listFrames()` and `page.frame(info)`
|
|
204
|
-
issue a Frame with the same document operations for that exact frame. None changes display
|
|
205
|
-
selection or opens a connection. An action's result `url` is its frame's http(s) address; a
|
|
206
|
-
document without one (`about:srcdoc`, `about:blank`, `data:`) reports its nearest ancestor
|
|
207
|
-
frame's, which is also its base URL. That address is resolved before dispatch, so an action never
|
|
208
|
-
fails over it after input landed: a target with no http(s) address anywhere in its frame tree, or
|
|
209
|
-
whose own http(s) address cannot be reported (longer than 8,192 characters, or carrying
|
|
210
|
-
credentials; an ancestor's address never stands in for it), is refused `Unsupported` and
|
|
211
|
-
`undispatched`, and an action that removes its own frame or closes its
|
|
212
|
-
page reports the address read before dispatch.
|
|
213
|
-
|
|
214
|
-
Pass an issued Page or Frame as the object you were given. Capture, page control, provider
|
|
215
|
-
transfers, agent tools and `Browser.checkPage` authenticate it by identity, so a spread copy
|
|
216
|
-
(`{ ...page }`), a wrapper or a Proxy is refused `UnregisteredSession`, even though methods copied
|
|
217
|
-
onto it still run. To instrument work, read `session.timeline` and `page.timeline`, which record
|
|
218
|
-
every operation's phases, and plan run receipts, instead of wrapping Page methods.
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
const stage = session.initialPage;
|
|
222
|
-
const scout = yield * session.createPage();
|
|
223
|
-
yield * scout.navigate({ url: scoutUrl });
|
|
224
|
-
const stageObservation = yield * stage.observe();
|
|
225
|
-
|
|
226
|
-
yield * scout.observe();
|
|
227
|
-
yield * scout.screenshot({ fullPage: false });
|
|
228
|
-
yield *
|
|
229
|
-
stage.clickElement({
|
|
230
|
-
observationId: stageObservation.observationId,
|
|
231
|
-
elementId: stageObservation.controls[0]!.elementId,
|
|
232
|
-
});
|
|
292
|
+
import { Effect } from "effect";
|
|
293
|
+
import { PolicyDenied } from "effect-browser/BrowserError";
|
|
294
|
+
import * as Chromium from "effect-browser/Chromium";
|
|
295
|
+
|
|
296
|
+
const browser = Chromium.layer({
|
|
297
|
+
guard: (request) =>
|
|
298
|
+
request.facts.includes("secret") && request.facts.includes("cross-origin")
|
|
299
|
+
? Effect.fail(new PolicyDenied({ detail: "Secrets are never sent to another site." }))
|
|
300
|
+
: Effect.void,
|
|
301
|
+
});
|
|
233
302
|
```
|
|
234
303
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
a closing page or a detached frame fails `Stale`, all `undispatched`. `page.status` is passive
|
|
244
|
-
host state, including terminal containment facts, and
|
|
245
|
-
remains readable after closure. Reconnect and handoff resume return a bounded `Inventory` with
|
|
246
|
-
the current generation and fresh Page metadata. Acquire a Page from that inventory and call
|
|
247
|
-
`page.observe()` explicitly; old capabilities stay stale. Native title and URL reads are
|
|
248
|
-
non-atomic. The separate `session.pages` lifecycle stream attaches its cached registry snapshot
|
|
249
|
-
and journal cursor together.
|
|
250
|
-
|
|
251
|
-
Observation storage is finite, including snapshots awaiting native disposal. Optional
|
|
252
|
-
`automation.observationLimits` sets all six bounds together: defaults are 16 snapshots,
|
|
253
|
-
8,192 handles and 64 MiB per page, and 64 snapshots, 32,768 handles and 256 MiB per session.
|
|
254
|
-
Reads reserve capacity before native allocation and return `Limit/undispatched` when it is
|
|
255
|
-
unavailable. Timeout or caller cancellation does not return a reservation until native work
|
|
256
|
-
and disposal settle, or exact positive page/connection retirement proves it unusable.
|
|
257
|
-
The snapshot bounds include temporary reads such as facts, screenshots and checkpoints. Leave
|
|
258
|
-
one slot beyond each retained frame observation for an action's fresh read; a one-slot bound
|
|
259
|
-
supports passive reads alone but cannot read facts or act while an observation occupies it.
|
|
260
|
-
Extraction reserves 4 KiB plus the requested text bytes and 512 KiB per requested control;
|
|
261
|
-
a picture checkpoint also reserves its picture allowance. A standalone screenshot reserves
|
|
262
|
-
4 KiB plus its returned-byte allowance. A plan step's targets reserve, per frame, one
|
|
263
|
-
descriptor read at a time (4 KiB plus 512 KiB) and then what each resolved control actually
|
|
264
|
-
retains, so a large form refuses only when its retained facts exceed the bound. Released native
|
|
265
|
-
resources reduce the retained reservation.
|
|
266
|
-
|
|
267
|
-
`page.ready({ timeoutMillis })`, `page.describe({ timeoutMillis })`,
|
|
268
|
-
`page.listFrames({ timeoutMillis })`, `page.close({ timeoutMillis })` and
|
|
269
|
-
`session.listPages({ timeoutMillis })` narrow the owner's deadline. `Capture.start(page, options)`
|
|
270
|
-
and `Capture.stream(page, options)` use the same owner and capture budget. With page control
|
|
271
|
-
enabled, `PageControl.state(page)`, `PageControl.suspend(page)` and
|
|
272
|
-
`PageControl.resume(page, receipt)` authenticate that exact Page and suspension receipt.
|
|
273
|
-
|
|
274
|
-
### Actions, plans and recording
|
|
275
|
-
|
|
276
|
-
Issued Pages and Frames expose `run(plan, options)` and scoped `start(plan, options)` on their original owner. `effect-browser/plan-data` supplies one tagged action schema for navigation, exact target input, selection, forms, scrolling and bounded waits. A live plan can name an observed Ref or a Descriptor; durable version-1 targets are descriptor-only, with authored literal values or named inputs. Recording replaces input literals with named slots. `Plan.make` validates live intent, `Plan.decode` validates stored intent, and `Plan.encode` serializes normalized defaults. Ingress checks finite data cost before semantic decoding: at most 128 distinct steps and 1 MiB of encoded data.
|
|
304
|
+
`effect-browser/Policy` builds guards for a browser with nobody watching. A judge reads what an
|
|
305
|
+
input means. `Policy.reviewer()` asks the `LanguageModel` in context for a structured review, and
|
|
306
|
+
`Policy.decider` asks a `DecisionModel`, such as Jev through `@effect/ai-typesafe`. Both give a
|
|
307
|
+
`Judgement`: for each `Risk` (`financial`, `account`, `access`, `deletion`, `communication` and
|
|
308
|
+
`secret`), the probability that the input does it, and the probability that the user's task asks
|
|
309
|
+
for it. `Policy.escalate(first, second)` asks `second` only when `first` is unsure.
|
|
310
|
+
`Policy.make({ judge })` denies an input with a likely risk the task does not ask for, saying which
|
|
311
|
+
and why. `origins` keeps input to some origins, and `deny` refuses facts outright:
|
|
277
312
|
|
|
278
313
|
```ts
|
|
279
|
-
import
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const ran =
|
|
283
|
-
yield *
|
|
284
|
-
page.run(
|
|
285
|
-
{
|
|
286
|
-
version: 1,
|
|
287
|
-
steps: [
|
|
288
|
-
{ id: "open", action: { _tag: "Navigate", url: "https://example.com" } },
|
|
289
|
-
{
|
|
290
|
-
id: "search",
|
|
291
|
-
action: {
|
|
292
|
-
_tag: "Fill",
|
|
293
|
-
target: { _tag: "Descriptor", descriptor: { kind: "input", label: "Search" } },
|
|
294
|
-
value: { _tag: "Input", name: "query" },
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
id: "quiet",
|
|
299
|
-
action: { _tag: "Wait", mode: { _tag: "Settled", quietMillis: 100, withinMillis: 2000 } },
|
|
300
|
-
},
|
|
301
|
-
],
|
|
302
|
-
},
|
|
303
|
-
{ inputs: { query: "Effect" }, within: "20 seconds", style: "plain" },
|
|
304
|
-
);
|
|
305
|
-
const stored = yield * Plan.recorded(ran);
|
|
306
|
-
const encoded = yield * Plan.encode(stored);
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
Every step has its own admission and action accounting. Form fields, verification and submit reuse the existing exact-node form implementation; a walk never holds a permit across its steps. `within` captures one absolute run bound, and each step captures its action and queue deadlines once. Resolution, form phases, conditions and optional checkpoints cannot renew those bounds. `through: stepId` returns a successful prefix and the next step's index and id; it does not automatically resume or replay that prefix. Missing input bindings or an invalid `through` fail preparation before any input.
|
|
310
|
-
|
|
311
|
-
Descriptors compare the full exact kind and label, optional destination and stable field metadata, checked ordinal/cardinality and a bounded main-anchored frame path. Scope normalizes to `document`; a recorded viewport descriptor uses document-wide uniqueness by default. Explicit `ViewportContext` resolution requires checked preconditions. Missing, ambiguous, incomplete or exhausted evidence refuses before input. Descriptor steps resolve private exact nodes within the step's admission without replacing the public observation. A live Ref always keeps its original identity and is never substituted.
|
|
312
|
-
|
|
313
|
-
`yield* page.resolve(descriptor, { guard })` performs an explicit admitted read and returns a fresh ordinary Ref. It retires that Frame's previous public observation on success. A Page cannot mint a child Frame's Ref: acquire the checked child Frame and resolve there. A plan on a Page can route a main-anchored child descriptor privately to that exact frame without changing selection.
|
|
314
|
-
|
|
315
|
-
`const operation = yield* page.start(plan, options)` returns a scoped handle before its worker is scheduled. Its `completed` Effect joins one result; repeated joins submit no new input. `operation.cancel` interrupts that worker. `operation.attempts` retains bounded host evidence after cancellation, scope exit or an outer race, including original causes and every native dispatch/acknowledgement phase. Run registrations are bounded to 32 per Page and 128 per session, with at most 1,024 phase rows per step. Evidence overflow prevents complete recording. `StepFailed` distinguishes preparation from an attempted step and retains the completed prefix, current attempt and original `BrowserError`. Its `message` names only the stage, step and reason; the retained steps can hold authored literal values, so log the message rather than the whole error. An acknowledgement survives later verification or postcondition failure; an unknown command stops the walk and keeps the original owner's containment outcome.
|
|
316
|
-
|
|
317
|
-
`Plan.recorded(ran)` projects only completely captured successful intent. Literal fill/type values become deterministic named input slots; existing input names and source conditions remain unchanged. `Plan.inputSlots(plan)` lists every named input a plan reads, with its step, action path and whether it is typed `text` or a replacement `value`, so a host binds `inputs` to replay a recording without the literal ever entering the stored plan. Failed or incomplete whole forms and unknown attempts are not reusable plans. After a later failure, `Plan.recorded(yield* operation.attempts, { through: completedStepId })` can explicitly retain an acknowledged completed prefix. Capture is based on checked native facts, and missing or omitted identity facts produce `Incomplete` rather than executable descriptor intent.
|
|
318
|
-
|
|
319
|
-
`Plan.recordedNavigation(operation, { id })` records a navigation started with `startNavigation`, such as the operation an agent-browser `Navigation` receipt holds, as a plan of one `Navigate` step named `id` (`"navigate"` by default). It joins `completed`, which stops nothing. Only a navigation that reached DOMContentLoaded is recorded; one that failed, was stopped or abandoned, or whose outcome is unknown fails `RecordingIncomplete` with `Unacknowledged`, as an unacknowledged run step does. The step holds `operation.request`, the address asked for, not the `NavigationResult.url` a redirect reached, so a replay repeats the request, as a recorded run's `Navigate` step does.
|
|
320
|
-
|
|
321
|
-
`yield* page.settled({ quiet: "100 millis", within: "2 seconds" })` uses one owned wait in the pinned document. It reports quiet DOM mutation, scroll, root geometry and viewport signals. It does not establish network, descendant animation or business completion. Native timers work on hidden pages, but browser throttling can still cause an honest timeout. Cancellation retains wait capacity until native observer disposal is confirmed. A page-initiated navigation that replaces the observed document ends the wait `Stale` and confirms that disposal, so the next wait can start. Plain plans use the same browser inputs as ordinary operations; presentation and recording composition stay with the host.
|
|
322
|
-
|
|
323
|
-
### Replayable session evidence
|
|
324
|
-
|
|
325
|
-
`session.timeline` publishes one bounded metadata journal for the active connection. An issued
|
|
326
|
-
`page.timeline` filters that same journal for its original Page. Snapshot reads and independent
|
|
327
|
-
subscribers acquire no browser permit, send no input and cannot stop native capture.
|
|
328
|
-
|
|
329
|
-
```ts
|
|
330
|
-
import * as Timeline from "effect-browser/timeline";
|
|
331
|
-
|
|
332
|
-
const history = yield * session.timeline.snapshot();
|
|
333
|
-
const tail = session.timeline.events(history.resumeAfter);
|
|
334
|
-
const appendTime = yield * session.timeline.now;
|
|
335
|
-
const fromTime = session.timeline.events({ at: appendTime });
|
|
336
|
-
const portable = yield * Timeline.encodeSnapshot(history);
|
|
337
|
-
```
|
|
314
|
+
import { Effect, Layer } from "effect";
|
|
315
|
+
import * as Chromium from "effect-browser/Chromium";
|
|
316
|
+
import * as Policy from "effect-browser/Policy";
|
|
338
317
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
terminal state and `resumeAfter`. A quiet Page's filtered snapshot still uses the global
|
|
343
|
-
watermark, including when it has no matching events. Evicting every retained event does not
|
|
344
|
-
reset that watermark. Requested evicted history and lapped subscribers fail `TimelineGap` with
|
|
345
|
-
requested and available cursors. Foreign store/clock identities and future cursors fail
|
|
346
|
-
`TimelineCursorError`; consumers choose how to recover rather than silently losing evidence.
|
|
347
|
-
An event the journal refuses as malformed or oversized leaves a `MetadataOmitted` marker with
|
|
348
|
-
its original target, correlation and tag, so a Page view sees its own gap. An omission whose
|
|
349
|
-
attribution was itself refused is unattributed and appears in every Page view.
|
|
350
|
-
|
|
351
|
-
`session.pages` is a lifecycle stream: it atomically attaches a copied Inventory baseline from
|
|
352
|
-
the canonical bounded native registry and the journal watermark, then follows lifecycle events.
|
|
353
|
-
Inventory metadata is explicitly cached: unread or omitted title/address fields are `null`.
|
|
354
|
-
It grants no live Page authority. `session.listPages()` remains the admitted fresh native
|
|
355
|
-
metadata read; its URL/title round trips are non-atomic. Restarting `session.pages` obtains a
|
|
356
|
-
fresh baseline after a gap. `Timeline.projectPages` and `Timeline.encodePages` remove cached
|
|
357
|
-
addresses, titles and native target IDs from the client projection. Native navigation and capture
|
|
358
|
-
boundaries retain bounded host addresses observed at their original callbacks; `Timeline.project`
|
|
359
|
-
and `Timeline.encodeClient` strip those nested fields too. `Timeline.encode` and `encodeSnapshot`
|
|
360
|
-
encode host evidence and preserve those addresses.
|
|
361
|
-
|
|
362
|
-
The session facade chooses the current journal when an Effect or Stream executes. Each active
|
|
363
|
-
subscription stays on that journal. Reconnect creates a new store identity; old subscribers
|
|
364
|
-
drain their available tail and terminate, or report a gap if lapped. A time selector earlier
|
|
365
|
-
than the current journal's start fails `TimelineGap`: that history belongs to the previous
|
|
366
|
-
journal, which an issued Page's timeline from that connection still reads. Issued Page timelines stay
|
|
367
|
-
on their original domain. Page/session terminal delivery does not bypass retention, and
|
|
368
|
-
`page.status` retains original containment facts after the corresponding events have evicted.
|
|
369
|
-
|
|
370
|
-
Events include plan correlation, original native phases, genuine input intervals, navigation,
|
|
371
|
-
settled/containment outcomes and capture references. A dispatch means handoff to checked native
|
|
372
|
-
implementation; acknowledgement means native completion. A performed preparatory or burst reply
|
|
373
|
-
carries `acknowledgement: { subphase, logicalComplete }`; only an acknowledgement without that
|
|
374
|
-
fact, or with `logicalComplete: true`, completed logical input. Later follow-up failure remains
|
|
375
|
-
separate. Key events contain counts and their units, never text. Individual pointer commands
|
|
376
|
-
are `Pointer` events; `Glide` is reserved for an actual bounded intended schedule. Capture IDs,
|
|
377
|
-
interval-local frame sequences and boundary attribution refer to the original capture; no
|
|
378
|
-
frame bytes enter the journal. `FirstFrame` means first accepted received frame after a
|
|
379
|
-
boundary, without proving new-document pixels. Picture native-call intervals remain separate
|
|
380
|
-
from DOM/text intervals and screencast receipt times.
|
|
381
|
-
|
|
382
|
-
Phase `nativeOrdinal` counts original dispatch phases within its `operationId`, rather than
|
|
383
|
-
individual CDP commands. `DisplayChanged` carries the selection and qualified running, held or
|
|
384
|
-
unknown display state from the same native callback.
|
|
385
|
-
|
|
386
|
-
Terminal retirement waits for original logical ticket/run outcomes and bounded capture stop
|
|
387
|
-
cleanup to publish their evidence. An unconfirmed native stop remains unconfirmed; retirement
|
|
388
|
-
does not wait indefinitely for raw native promises. Later native callbacks stay pinned to the
|
|
389
|
-
old journal and can be refused once it has retired. Capture `latePhase` qualifies a Watching or
|
|
390
|
-
Started callback after the logical interval ended. Original capture snapshots and quarantine
|
|
391
|
-
retain their independent native stop and loss facts.
|
|
392
|
-
|
|
393
|
-
Append timestamps come from the owner's captured host Clock and order with global sequences.
|
|
394
|
-
Receipt/source times are payload facts and never backdate append order. `Stamp.offsetNanos`
|
|
395
|
-
is relative to that clock domain's origin; its `clockId` is required. Explicit `*Json` schemas
|
|
396
|
-
and Timeline encoding helpers represent bigint offsets/sequences as decimal strings. Native
|
|
397
|
-
presentation Unix milliseconds remain a separately qualified source clock.
|
|
398
|
-
|
|
399
|
-
`timelineLimits` on Chromium/Browserbase options, or `automation.timelineLimits` on the runtime,
|
|
400
|
-
bounds retained metadata and subscriptions. Defaults are 60 seconds, 4,096 events, 4 MiB,
|
|
401
|
-
32 subscribers and 64 KiB per event. Configurable maxima are six hours, 65,536 events, 64 MiB,
|
|
402
|
-
256 subscribers and 1 MiB per event. Metadata-byte and per-event limits have a 2 KiB minimum so
|
|
403
|
-
canonical terminal and omission evidence fits. Count/time/byte eviction runs on append and read. Subscribers
|
|
404
|
-
retain bounded cursor/wakeup state rather than event queues; a slow consumer cannot backpressure
|
|
405
|
-
an admitted native action. Returned values are ordinary immutable snapshots, without an archive
|
|
406
|
-
registry retaining old journals.
|
|
407
|
-
|
|
408
|
-
### Admission and deadlines
|
|
409
|
-
|
|
410
|
-
Ordinary operations on one Page share its permit, including operations on its Frames. Work on
|
|
411
|
-
other Pages can proceed independently: a held scout read or typing response does not block
|
|
412
|
-
stage input, a stage checkpoint or another Page's capture. Creation, inventory and global
|
|
413
|
-
lifecycle operations use the registry permit. Inventory is bounded and non-atomic. So does issuing:
|
|
414
|
-
`session.page(info)` and `page.frame(info)` authenticate and register, so they never wait for, or
|
|
415
|
-
fail `Busy` on, work already running on that page. `page.describe()`, `page.listFrames()` and
|
|
416
|
-
`Capture.start` read the page and share its permit like other reads; pass `admission.queue` to
|
|
417
|
-
wait behind that work.
|
|
418
|
-
|
|
419
|
-
Every admitted operation accepts trailing host-only `OperationOptions`:
|
|
318
|
+
const browser = Layer.unwrap(
|
|
319
|
+
Effect.gen(function* () {
|
|
320
|
+
const judge = yield* Policy.reviewer();
|
|
420
321
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
322
|
+
return Chromium.layer({ guard: Policy.make({ judge, origins: ["https://shop.example"] }) });
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
424
325
|
```
|
|
425
326
|
|
|
426
|
-
|
|
427
|
-
`
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
327
|
+
The task comes from `Policy.Task`, which `effect-agent-browser`'s tools provide to their inputs
|
|
328
|
+
from the run's `task`; elsewhere, provide it yourself, or risky input is denied for want of one. A judge sees the task, the action, the typed
|
|
329
|
+
text and the facts as trusted, and the page's text only as evidence. It never sees the agent's own
|
|
330
|
+
words. A judgement only adds to structure: a `secret` fact counts whatever the judge reads. A judge
|
|
331
|
+
that fails or exceeds `timeout` (30 seconds) leaves the input unjudged. `make` then denies the input
|
|
332
|
+
if it has any fact, with the judge's failure as the `PolicyDenied` cause, and lets an input without
|
|
333
|
+
facts, such as a same-origin link, go ahead. Through `@effect/ai-openrouter` 4.0.0, structured output needs
|
|
334
|
+
`strictJsonSchema: true` in the model's config, or OpenRouter drops the response format and every
|
|
335
|
+
review fails to decode.
|
|
336
|
+
|
|
337
|
+
Holds use `policyTimeout`, a finite positive duration defaulting to five minutes, separately from
|
|
338
|
+
the action timeout. They do not keep the page locked. After approval, the library verifies the same
|
|
339
|
+
document, target and relevant facts before sending input: a control's name is bound but other page
|
|
340
|
+
text, such as a live price, is not; the URL is bound without a fragment that only marks a place on
|
|
341
|
+
the page, as scroll-spy and feed pages rewrite while scrolling (a `#/` or `#!` hash route stays
|
|
342
|
+
bound). Changed targets fail undispatched; the library never retries the action or the policy
|
|
343
|
+
automatically. A pointer press is checked again once the pointer has arrived and the page has had a
|
|
344
|
+
frame to react: the approved control must still receive the press point, so a control that appears
|
|
345
|
+
under the pointer, such as a hover menu, stops the action before the button goes down. A link,
|
|
346
|
+
button or other control nested inside the target between it and the press point stops it too, since
|
|
347
|
+
the approval inspected the target, not that control. Each
|
|
348
|
+
further press of a double or triple click is checked the same way after the earlier clicks' handlers
|
|
349
|
+
have run. Typing approves its field once. Plain text then goes into it in one insertion, which no
|
|
350
|
+
key handler can split, so the page sees one `input` event and no keys, and a paste's time does not
|
|
351
|
+
grow with its length: 2,000 characters took half a second at a 70 ms round trip. Typing key by key,
|
|
352
|
+
as a presenter's view does, checks that the approved control still has focus once the keys before
|
|
353
|
+
are answered, before each space, which could press a focused button, and after its last key, so a
|
|
354
|
+
key handler that moves focus stops the typing before its next space; each check is about two
|
|
355
|
+
protocol round trips, and the typing deadline allows 250 ms for it. A policy timeout is a typed `PolicyTimeout`, and tools surface both timeout and denial as ordinary failed
|
|
356
|
+
receipts. Without a guard, actions are allowed and nothing is revalidated. Canvas and opaque frames
|
|
357
|
+
expose their outer element's metadata.
|
|
358
|
+
|
|
359
|
+
A presenter's view reaches an off-screen ref target with visible wheel input before the pointer
|
|
360
|
+
moves to it. Scroll attempts are bounded and may use one instant fallback. A denied or held action
|
|
361
|
+
does not scroll. After scrolling, a guarded action checks the original target again; a page handler
|
|
362
|
+
that changes its meaning can therefore stop it after its wheel input but before a click, and the
|
|
363
|
+
failure is undispatched: travel toward a press is not the action's input. Drag endpoints are
|
|
364
|
+
resolved together in the final viewport, and checked under the pointer, before the button is
|
|
365
|
+
pressed.
|
|
366
|
+
|
|
367
|
+
A view's pointer follows the planner its presenter was given, `Motion.lognormal` unless another.
|
|
368
|
+
That tuned two-stroke sigma-lognormal model is evaluated every 16.7 ms, but a move goes only when
|
|
369
|
+
the pointer reaches a new pixel; the exact destination lands at the model's end time, so only that
|
|
370
|
+
final move can repeat the position before it. A custom `plan(from, to)` returns a complete
|
|
371
|
+
schedule with finite coordinates and nondecreasing absolute `afterMillis` offsets, at most 2,048
|
|
372
|
+
samples and 5,000 milliseconds, ending at the exact destination. The browser decodes it with
|
|
373
|
+
`Motion.Plan` into its own copy, then checks the endpoint; invalid plans fail with
|
|
374
|
+
`InvalidRequest` before their track or input is sent. Equal-time samples are retained. Plain input
|
|
375
|
+
never plans a glide: its pointer jumps, or moves in eight short steps while dragging.
|
|
376
|
+
|
|
377
|
+
For recorded human strokes, install the optional `effect-browser-human-strokes` package and give
|
|
378
|
+
its planner to the presenter:
|
|
467
379
|
|
|
468
380
|
```ts
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const navigateStage = stage.navigate({ url: stageUrl });
|
|
381
|
+
import * as HumanStrokes from "effect-browser-human-strokes";
|
|
382
|
+
import * as Presentation from "effect-browser/Presentation";
|
|
472
383
|
|
|
473
|
-
yield *
|
|
474
|
-
yield * navigateStage; // still navigates the original stage
|
|
475
|
-
const seen = yield * scout.observe({ scope: "viewport" });
|
|
384
|
+
const presenter = yield * Presentation.make({ motion: yield * HumanStrokes.motion });
|
|
476
385
|
```
|
|
477
386
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
and the
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
when it is occupied. Pass `{ admission: { queue: "1 second" } }` to wait FIFO within the same
|
|
514
|
-
operation deadline. It does not wait behind another Page's ordinary operation. A refused or
|
|
515
|
-
expired caller opens nothing.
|
|
516
|
-
|
|
517
|
-
Inventory authenticates each native Page before adopting it into the owner's registry,
|
|
518
|
-
including popups. `page.describe()` reads its current address and title without reading every
|
|
519
|
-
other page. `session.listPages()` reads every page's title concurrently under bounded admission.
|
|
520
|
-
Metadata does not itself grant live input authority.
|
|
387
|
+
That package bundles 32,130 attributed CC BY 4.0 strokes, preserving their original sample times.
|
|
388
|
+
The core package includes no stroke data. Every glide reserves its full bounded schedule before
|
|
389
|
+
its published clock starts, so delayed replies cannot stretch a dense stroke through backpressure.
|
|
390
|
+
At most 2,112 input commands and reservations are owned at once; ordinary input retains its
|
|
391
|
+
64-command admission limit. Actions still await their replies before succeeding, and interruption
|
|
392
|
+
stops the unsent suffix and releases held input.
|
|
393
|
+
|
|
394
|
+
Typing sends key pairs for printable US characters, plainly and in a view; other text uses
|
|
395
|
+
Unicode insertion. A typed space or letter can press a focused button, toggle a box, follow a
|
|
396
|
+
link or change a select, so `type` refuses before any input when its `into` ref is not a text field
|
|
397
|
+
or, without `into`, when focus is on such a control; `press` sends keys to those. With `secret`,
|
|
398
|
+
`type` also refuses a field the page does not mark secret, so a password goes only where the page
|
|
399
|
+
hides it, as a replayed password does. A view types at its pacing's words a minute, 70 by
|
|
400
|
+
default, including slower word starts, with key holds around 110 ms that can overlap. The ordered schedule releases a repeated
|
|
401
|
+
physical key before pressing it again. Keys follow that schedule without waiting for each network
|
|
402
|
+
reply. Pending replies are bounded and drained before an action succeeds; interruption stops new
|
|
403
|
+
input and releases every submitted held key. Shortcut chords retain Playwright’s platform-specific
|
|
404
|
+
editing behavior. Before each key, typing and repeated presses check that the page is still in the
|
|
405
|
+
document the action began in, and stop with a dispatched `NotActionable` once it has moved on. The
|
|
406
|
+
browser reports a new document as it commits, so a key sent within about one protocol round trip of
|
|
407
|
+
that commit can still reach it. Under a guard, typing's submit Enter and each repeated Enter or
|
|
408
|
+
Space are first checked against the approved element.
|
|
409
|
+
|
|
410
|
+
After a click, a key, a submit or a scroll, the page settles before the action returns: one call
|
|
411
|
+
waits a task and a frame in the page, and Chromium answers it only once a navigation the input
|
|
412
|
+
asked for, by a link, a form or a handler's timer, has committed. A document that committed is then
|
|
413
|
+
waited for until it is parsed, within 5 seconds. `pushState` and a 204 answer wait for nothing. A
|
|
414
|
+
navigation a handler starts once a fetch answers comes too late for any wait; the next look sees
|
|
415
|
+
it, and a picture never shows a document the page has left. A tab the input opens is discovered and
|
|
416
|
+
registered before the action returns, within its action timeout. An unresolved opening fails with
|
|
417
|
+
dispatched input, and closing the page or losing its browser wakes the wait with `Closed`.
|
|
418
|
+
|
|
419
|
+
## Presenting pages
|
|
420
|
+
|
|
421
|
+
`Presentation` performs input for viewers of a page, and `Stage` decides which page they see.
|
|
521
422
|
|
|
522
423
|
```ts
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
yield * session.selectPage(scout);
|
|
529
|
-
yield * stage.click({ selector: "#advance" });
|
|
530
|
-
const childText = yield * child.readText({ selector: "#status" });
|
|
531
|
-
```
|
|
424
|
+
import { Effect, Stream } from "effect";
|
|
425
|
+
import { Browser } from "effect-browser/Browser";
|
|
426
|
+
import type { Frame } from "effect-browser/Frame";
|
|
427
|
+
import * as Presentation from "effect-browser/Presentation";
|
|
428
|
+
import * as Stage from "effect-browser/Stage";
|
|
532
429
|
|
|
533
|
-
|
|
534
|
-
exact connection-local page/frame authority. Closing its Page, detaching its Frame or reconnecting
|
|
535
|
-
makes it stale. Navigation preserves the Page/Frame authority and retires that document's node
|
|
536
|
-
references. Holds and in-flight navigation reservations are checked on the issued target.
|
|
537
|
-
|
|
538
|
-
IDs are opaque and namespaced per connection. After reconnect, read fresh `listPages()`, match
|
|
539
|
-
exactly one saved native `targetId`, issue `session.page(freshInfo)`, and reacquire its Frames with
|
|
540
|
-
`page.listFrames()` / `page.frame(info)`. Zero matches means gone and multiple matches mean
|
|
541
|
-
ambiguous. Never fall back to ordering, local serial, URL or title. A surviving native target
|
|
542
|
-
cannot make an old Page or copied metadata current again.
|
|
543
|
-
|
|
544
|
-
Dispatched input on the observation's own page still retires its references, including `hover`,
|
|
545
|
-
`pointerMove`, `wheel` and `scroll`. Hover then click therefore deliberately needs a new inspection.
|
|
546
|
-
Page scripts can replace nodes or change relevant state in response to any input; no verb is
|
|
547
|
-
assumed harmless. Navigation or closure on that page and reconnect also retire its observation,
|
|
548
|
-
even while another page is selected. IDs are opaque and connection-specific; consume the actual
|
|
549
|
-
inspection result rather than predicting serials. Attachment, identity and fresh-state checks at
|
|
550
|
-
dispatch remain necessary even when the last input targeted a different page.
|
|
551
|
-
|
|
552
|
-
### Performed plans and absolute starts
|
|
553
|
-
|
|
554
|
-
`page.run(plan, { style: {} })` uses the named default performed profile; omitted style or
|
|
555
|
-
`style: "plain"` keeps plain input. `Plan.checkRunOptions(value)` checks and normalizes host
|
|
556
|
-
options, preserving public `within`, queue admission and performed defaults. Profiles have finite
|
|
557
|
-
pointer duration/inset/curvature, key interval/hold and scroll duration/interval bounds. `seed` is
|
|
558
|
-
an optional safe integer; the actual chosen seed is retained in run and attempt receipts.
|
|
559
|
-
Step-local planning depends on that seed and stable step ID, independently of other Pages or
|
|
560
|
-
ambient Random use. It reproduces planned paths for the same geometry, not website behavior or
|
|
561
|
-
remote delivery timing. Slips default to probability zero and require explicit opt-in.
|
|
562
|
-
|
|
563
|
-
The pointer timing uses a chosen bounded Shannon-form policy for measured targets, and explicit
|
|
564
|
-
coordinates use a chosen bounded distance policy. The fifth-order progress polynomial gives zero
|
|
565
|
-
endpoint velocity/acceleration; applying it to a curved policy path does not claim globally minimum
|
|
566
|
-
Cartesian jerk. Constants are library policy, not calibrated human guarantees. Scheduled glides are
|
|
567
|
-
output-only compositor metadata: they do not generate hover along the drawn path. Exact native hit
|
|
568
|
-
checks and selected node identity remain required immediately before real input. A missing actual
|
|
569
|
-
click position stays null even when an intended aim is retained.
|
|
570
|
-
|
|
571
|
-
A schedule has at most 128 geometry samples per path or scroll, and performed text at most 256
|
|
572
|
-
Unicode code points, which expand to at most 768 strokes (a slip adds a wrong key and its
|
|
573
|
-
Backspace), independently of action and pending-native capacity. Longer text fails `Limit` with
|
|
574
|
-
dimension `code-points` and its measured count before any input. Performed Fill
|
|
575
|
-
may focus/select/replace inside its action; Type still requires existing focus. Each performed
|
|
576
|
-
logical action costs the same action budget as its plain counterpart. FillForm retains its
|
|
577
|
-
per-field admission, verification, partial results and guarded submit.
|
|
578
|
-
|
|
579
|
-
`startAt` is an absolute bigint on this owner's host monotonic clock, available through
|
|
580
|
-
`session.monotonicTimeNanos` and, for a host that holds only an issued Page or Frame, the same
|
|
581
|
-
clock's `page.monotonicTimeNanos`. Future starts wait before admission, reserve no Page permit and
|
|
582
|
-
never begin before that instant on the owner's clock, so `latenessNanos` is never negative.
|
|
583
|
-
A waiting start holds only its run registration, never the timeline: when its Page closes or its
|
|
584
|
-
owner pauses, detaches or ends, the run fails preparation at once instead of at its start.
|
|
585
|
-
`within` starts at the intended boundary and bounds queueing, preparation, pacing, input and
|
|
586
|
-
postconditions together, capped by owner lifetime and each action deadline. Missed preparation
|
|
587
|
-
reports `ScheduleMissed` without inventing an attempt; an insufficient pacing budget reports
|
|
588
|
-
`TimingBudgetExceeded` before new input. Performed strokes start at their absolute schedule
|
|
589
|
-
offsets, so a slow reply delays one stroke rather than every later one. Before each stroke's
|
|
590
|
-
first key, the rest of the schedule must still fit the deadline at the round trips the browser
|
|
591
|
-
has actually taken; a stroke that could not finish is refused whole, and strokes already
|
|
592
|
-
acknowledged stay `performed`. A performed click is held to the same rule. Playwright's click
|
|
593
|
-
checks the node again before its first input event, and a deadline inside those checks would
|
|
594
|
-
report `unknown` for input never sent, so a click the deadline cannot fit at the press's
|
|
595
|
-
measured round trip is refused `TimingBudgetExceeded`, undispatched, and its Page stays open.
|
|
596
|
-
Receipts retain requested/intended/actual start, deadline
|
|
597
|
-
and lateness. Cancellation stops future submissions and keeps readable attempt history with the
|
|
598
|
-
original Effect Cause. Known preparatory work followed by refusal is rejected with its subphase
|
|
599
|
-
receipts; pending input remains unknown, and acknowledged logical input stays performed when a
|
|
600
|
-
later check fails.
|
|
601
|
-
|
|
602
|
-
### An unknown outcome on an exact Page
|
|
603
|
-
|
|
604
|
-
A mutation whose outcome becomes unknown after dispatch, because it timed out, failed or was
|
|
605
|
-
interrupted, never runs again automatically. When the owner can attribute it to an exact Page,
|
|
606
|
-
it revokes that Page's admission and asks the browser to close it within two seconds. This
|
|
607
|
-
includes issued Page/Frame operations, forms, file transfers,
|
|
608
|
-
navigation, initialization and callback retirement. Concurrent close requests join the same
|
|
609
|
-
owned closure attempt. Once the browser confirms closure, healthy Pages, their observations and
|
|
610
|
-
captures can continue. The original operation still fails with its own reason and `unknown`
|
|
611
|
-
outcome, carrying `PageClosed` containment; its Page and Frame authority remains terminal.
|
|
612
|
-
`session.diagnostics` records `page-contained`.
|
|
613
|
-
|
|
614
|
-
Closing the page does not undo what the operation already did. A request it made, a cookie or
|
|
615
|
-
storage it changed, a download or a popup it started can all outlast the page, exactly as they
|
|
616
|
-
can after a known outcome. Unconfirmed closure or a failure without trustworthy Page attribution
|
|
617
|
-
fences the session conservatively and reports `SessionFenced`. A late response never reopens
|
|
618
|
-
fenced control. Containment reports owner safety independently of the original action outcome.
|
|
619
|
-
|
|
620
|
-
The complete [multi-page example](examples/multi-page.ts) keeps an issued presentation Page while
|
|
621
|
-
the selected scout supplies observations. It reads and captures the presentation page without
|
|
622
|
-
switching selection and returns only data after the shared browser scope closes.
|
|
623
|
-
|
|
624
|
-
### Typed host failures
|
|
625
|
-
|
|
626
|
-
`BrowserError` keeps its operation and a required `outcome` (`undispatched`, `rejected`, `performed`, or
|
|
627
|
-
`unknown`), while `reason` is a tagged union. Recover by reason without parsing strings:
|
|
430
|
+
declare const encode: (frame: Frame) => Effect.Effect<void>; // the application's own output
|
|
628
431
|
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
.
|
|
632
|
-
|
|
432
|
+
const program = Effect.gen(function* () {
|
|
433
|
+
const stage = yield* Stage.make({ quality: 80 });
|
|
434
|
+
const presenter = yield* Presentation.make();
|
|
435
|
+
const page = yield* (yield* Browser).newPage("https://example.com");
|
|
436
|
+
yield* stage.frames.pipe(Stream.runForEach(encode), Effect.forkScoped);
|
|
437
|
+
const shown = yield* stage.present(page); // Presented { page, at, latency }
|
|
438
|
+
yield* presenter.view(page).click("e12"); // glides, then clicks, as a person would
|
|
439
|
+
});
|
|
633
440
|
```
|
|
634
441
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
`navigate` holds nothing open that you can see into: it returns when the document reaches DOMContentLoaded. `startNavigation` is the same single dispatch, left in flight, so a recorder can look at a page while it is still arriving, hold it, and let it finish:
|
|
652
|
-
|
|
653
|
-
Both accept optional `timeoutMillis` from 1 to 600000, capped by the remaining session lifetime.
|
|
654
|
-
Omitting it uses the configured action timeout. Page and Frame calls share the
|
|
655
|
-
same navigator; the maintained model `browser_navigate` tool still accepts only its upstream URL
|
|
656
|
-
request. This field is the loading deadline, not a promise of rollback on timeout. On a main-frame
|
|
657
|
-
timeout from the pinned engine, up to 3000 ms of recovery may follow that deadline. One absolute
|
|
658
|
-
recovery deadline includes owner-permit waiting, native setup and acknowledgement, and is capped
|
|
659
|
-
by the remaining session lifetime. It is never renewed and cannot extend that lifetime.
|
|
442
|
+
A presenter owns one drawn pointer. `presenter.view(page)` is the page with its actions performed:
|
|
443
|
+
each waits first as a person reacts, by `Presentation.human`'s medians, 280 ms after an expected
|
|
444
|
+
change, 600 ms after a new document and 1 second on another page; the pointer glides from where
|
|
445
|
+
viewers last saw it, on whichever page; a field is clicked before typing; and the wheel turns in
|
|
446
|
+
100 px notches, in bursts of up to nine. One view acts at a time. The time a view spends showing an
|
|
447
|
+
action, its glides, holds and typing, has a budget of its own outside `actionTimeout`. `page`
|
|
448
|
+
itself stays plain, and plain input on any page never waits for a view. `view.aim(target)` starts
|
|
449
|
+
the glide as soon as a target is known, as when it appears in a model's streamed tool call; the
|
|
450
|
+
view's next action completes it if it acts on that target, and stops it where it is otherwise. An
|
|
451
|
+
aim records no action, and does nothing under an input guard.
|
|
452
|
+
|
|
453
|
+
A stage is one per live output. `stage.cue(page)` starts capture now; `cue.ready` waits for a
|
|
454
|
+
current frame, and `cue.commit({ at })` switches on the host time of an event that actually
|
|
455
|
+
happened. Preparing belongs to the caller's scope; once committed, capture belongs to the stage.
|
|
456
|
+
Closing an abandoned cue stops its capture. `present(page, { at })` is the convenience for cueing
|
|
457
|
+
and committing in one call.
|
|
660
458
|
|
|
661
459
|
```ts
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
yield * PageControl.resume(page, receipt);
|
|
668
|
-
const { url: loaded } = yield * operation.completed;
|
|
460
|
+
// Inside a scoped program: lineSent yields Some(hostTime) when the line goes out, or None if dropped.
|
|
461
|
+
const cue = yield * stage.cue(page);
|
|
462
|
+
yield * cue.ready;
|
|
463
|
+
const sent = yield * lineSent;
|
|
464
|
+
if (Option.isSome(sent)) yield * cue.commit({ at: sent.value });
|
|
669
465
|
```
|
|
670
466
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
467
|
+
Commits serialize in call order. A switch waits until `at` and for admitted old-page input and
|
|
468
|
+
its native replies, keeping new old-page input queued through the cut. Reads and capture continue.
|
|
469
|
+
The consumer can stop performance between `Plan.take` steps for a clean cut. Interruption before
|
|
470
|
+
the switch changes nothing and leaves the cue usable. A committed switch keeps ownership through
|
|
471
|
+
outgoing capture cleanup, even if its caller is interrupted then.
|
|
472
|
+
|
|
473
|
+
`Presented.at` records when the switch took effect, no earlier than the event, the incoming frame
|
|
474
|
+
or the last frame shown; `latency` is its delay after the requested `at`. `firstFrame` bounds a
|
|
475
|
+
wait for current paint (5 seconds by default). Old-page input admission, drain and the final frame
|
|
476
|
+
wait are also bounded by that browser's `navigationTimeout`. Failure before the cut leaves the
|
|
477
|
+
old output in place. An old document's frame cannot satisfy a new document's readiness; paint-time
|
|
478
|
+
uncertainty must place it after the document commit. A cue joining an existing quiet capture can
|
|
479
|
+
use that capture's current frame without waiting for invented new paint.
|
|
480
|
+
|
|
481
|
+
`stage.shown({ hold: "1 second" })` describes the output without inventing new frames:
|
|
482
|
+
|
|
483
|
+
- `Painted` carries a frame when it is published to the output.
|
|
484
|
+
- `Still` follows a quiet interval only after a fresh page/capture round trip, with no pending
|
|
485
|
+
capture acknowledgement, the same active capture and current document, and no newer frame.
|
|
486
|
+
- `Stalled` says silence could not be confirmed. A timeout is uncertainty, not stillness.
|
|
487
|
+
- `Gone` says the page or browser is lost. It is emitted once for that feed, with nothing more
|
|
488
|
+
until another page is committed.
|
|
489
|
+
|
|
490
|
+
`hold` must be finite and positive. Continuous paint restarts the silence interval. A failed
|
|
491
|
+
capture can restart while its page/browser still stands, but old paint cannot pass for a new
|
|
492
|
+
capture's first frame. These guarantees depend on Chromium reporting its paint; they do not
|
|
493
|
+
prove that every canvas update was delivered. `frames` remains the paint-only stream with bounded
|
|
494
|
+
reader queues, so subscribe before presenting to receive the first image. The consumer owns
|
|
495
|
+
encoding, redaction, slates and any repetition of a confirmed held image.
|
|
496
|
+
|
|
497
|
+
Local Chromium verifies preparation, ownership, input barriers and evidence. Earlier hosted
|
|
498
|
+
capture measurements do not establish these cued-presentation contracts: the H1 overlap/load
|
|
499
|
+
probe remains unrun until included Browserbase allowance is verified.
|
|
500
|
+
|
|
501
|
+
`Browser.now`, event stamps, frame `receivedAt` and a window's bounds share host monotonic milliseconds
|
|
502
|
+
from the clock captured when the browser is made. They remain ordered across wall-clock corrections.
|
|
503
|
+
Page operations also pace input and measure their deadlines on that clock, so a caller running
|
|
504
|
+
under another `Clock`, such as a `TestClock`, cannot stall an action or a page's turns.
|
|
505
|
+
Compare these stamps only within that clock: they are not epoch dates or comparable across hosts.
|
|
506
|
+
`Frame.timing` distinguishes `BrowserPaint` from `Screenshot`. Native frames retain browser epoch
|
|
507
|
+
milliseconds in `timestamp` and map them to `hostTime`, with an explicit clock uncertainty.
|
|
508
|
+
Screenshot fallbacks have only a host capture interval; their `timestamp` getter is undefined.
|
|
509
|
+
Windows and a moment's frame captions use `hostTime`, so delayed delivery cannot make old paint current.
|
|
510
|
+
A picture states how old it may be. `Page.frame({ maxAge, after })` serves the newest screencast
|
|
511
|
+
frame when it has the viewport's size and was painted at most `maxAge` ago, at the earliest its
|
|
512
|
+
timing allows: 250 ms by default, while 0 always takes a new screenshot. With `after: "input"` the
|
|
513
|
+
frame must also follow the page's latest submitted input, including input of an interrupted action,
|
|
514
|
+
and the read waits for the action in flight: what a caller that has just acted needs.
|
|
515
|
+
`Page.screenshot` always applies that rule, so a caller that acts and then looks sees what its
|
|
516
|
+
action did; a deck or a narrator that wants a picture of a stated age reads `frame`, which a frame
|
|
517
|
+
that qualifies serves at once, even while an action runs. A screencast sends only changes and can
|
|
518
|
+
miss a final paint, so a frame is only ever as current as its age. No read reuses a frame painted
|
|
519
|
+
before the page's current document began, when the page's own session saw its main frame commit
|
|
520
|
+
it: a page that keeps painting while the next document loads would otherwise leave its own frames
|
|
521
|
+
the newest when the navigation returns. Otherwise a new screenshot is
|
|
522
|
+
taken, which `frame` returns as a `Screenshot`-timed frame. A screenshot, which the agent's
|
|
523
|
+
observations take, and a `Moment`'s picture use `after: "input"`, so a stopped capture, a lost
|
|
524
|
+
final paint or later input never presents older paint as the page an action left.
|
|
525
|
+
|
|
526
|
+
A new picture goes on the page's own protocol session. Where a device pixel is a CSS pixel and
|
|
527
|
+
nothing is cropped, it is one `Page.captureScreenshot`; a crop, or another device pixel ratio, adds
|
|
528
|
+
the page's layout metrics for Playwright's clip formula. Where Playwright knows no viewport, as in
|
|
529
|
+
the default context over CDP, the page's first picture also learns it. Chromium takes a clipped
|
|
530
|
+
picture through the device emulation of the session that asks for it, then restores the view's size
|
|
531
|
+
and what that session emulated, so a page's pictures go one at a time. Where Playwright emulates the
|
|
532
|
+
viewport, as `Chromium.layer` does, a crop or a scaled picture is Playwright's own screenshot, on its
|
|
533
|
+
own session, which sends Playwright's 335 KB injected script with a document's first one, as it
|
|
534
|
+
would over CDP once a caller gives Playwright a viewport. Elsewhere, as on Browserbase, whose own
|
|
535
|
+
session emulates the screen, the page's own session holds a copy of the screen the page reads from
|
|
536
|
+
the page's first clipped picture on, at two calls once, so a crop leaves the screen as it was, and
|
|
537
|
+
`device-width` media queries with it. The copy is the screen as the page read it then, and every
|
|
538
|
+
later picture restores it, so a session that changes the screen afterwards sees its change undone.
|
|
539
|
+
When the library's session leaves the page, as when a kept session's process ends, Chromium clears
|
|
540
|
+
the emulation, as a crop used to.
|
|
541
|
+
|
|
542
|
+
Reads go to the page script in an isolated world. The page's own session registers the script at the
|
|
543
|
+
library's first read of the page, so every later document runs it from its start: a document's
|
|
544
|
+
first read takes two round trips and later reads one. Tabs the library only tracks are left alone,
|
|
545
|
+
and the clock probe runs in a world of its own.
|
|
546
|
+
Every operation is recorded as an `Action`, also when its caller interrupts it. An element or point
|
|
547
|
+
action records its `subject`, and a drag where it ended (`to`): the role, accessible name, tag and
|
|
548
|
+
context of what it found, read when its target was resolved, before the pointer moved or any input
|
|
549
|
+
was sent, so a click that removes its element still names it: the same subject `Page.find` gives. A ref is
|
|
550
|
+
reused by later outlines, so read `subject` rather than resolving `target` against a later snapshot.
|
|
551
|
+
|
|
552
|
+
Every renderer reads the same host wall clock, so the browser keeps one clock mapping for all of its
|
|
553
|
+
pages. Opening a browser measures nothing: the browser's first capture measures the mapping, with
|
|
554
|
+
three read-only probes in a world of their own, and waits for that one estimate. A busy page answers
|
|
555
|
+
each probe late, behind its own work, so the probes it answered within two seconds serve: the
|
|
556
|
+
estimate is as uncertain as the page made the wait, and the capture fails, undispatched, only if
|
|
557
|
+
the page answered none. A later capture starts with the estimate there is. While frames flow, a
|
|
558
|
+
capture measures again once the estimate is ten seconds old, and every frame is timed by the
|
|
559
|
+
browser's newest estimate, so a wide first estimate narrows once a probe finds the page idle, and
|
|
560
|
+
hours on air leave frames as certain as a fresh capture's. An estimate says less about the offset
|
|
561
|
+
as it ages, by up to 100 parts per million of its age, and a frame carries its estimate's
|
|
562
|
+
uncertainty at its paint. A new measurement
|
|
563
|
+
replaces the estimate only if it says more about the offset than the aged one, or if its interval
|
|
564
|
+
cannot contain the current offset (the clocks moved); a probe slowed by one busy tab cannot skew
|
|
565
|
+
every tab's stamps. Transport asymmetry remains in the reported uncertainty.
|
|
566
|
+
|
|
567
|
+
Once a capture has mapped the clock, mouse input and raw text-key events carry the epoch time each
|
|
568
|
+
was meant for; until then Chromium stamps them as it receives them, and input never waits for a
|
|
569
|
+
measurement. Shortcut chords retain Playwright's platform behavior, and Unicode insertion has no
|
|
570
|
+
timestamp field.
|
|
571
|
+
|
|
572
|
+
`Page.captureStats()` counts, over the page's life or with `{ window }` over up to the latest
|
|
573
|
+
minute: received and accepted frames, missing timestamps, `late` frames (Chromium encodes several at
|
|
574
|
+
once and finished these after a newer one; they are dropped, never reordered, and a busy machine
|
|
575
|
+
makes more of them), frames dropped for their size (`foreignSize`) or for coming during one of the
|
|
576
|
+
library's own clipped pictures (`duringPictures`), frames a reader `lost` by falling behind, and
|
|
577
|
+
paint-time gap totals/minimum/maximum/last. `ackBacklog` is the acknowledgements still unanswered:
|
|
578
|
+
Chromium sends a frame only while few are, so a backlog over a slow connection means fewer frames.
|
|
579
|
+
Chromium draws a clipped or scaled screenshot of the page, taken
|
|
580
|
+
from any session, into its running screencast. Readers never receive a frame whose picture has
|
|
581
|
+
another shape than its device. Frames of another device size wait, in order: the page's own size
|
|
582
|
+
returning drops them, while the page reporting a viewport of their size, or their lasting a second,
|
|
583
|
+
delivers them as a real resize, so a page that cannot answer, or that measures in other units as
|
|
584
|
+
under browser zoom, is never stalled. `foreignSize` therefore also counts some of the page's own
|
|
585
|
+
frames: those of a new size still unconfirmed when another size replaced it, when the capture
|
|
586
|
+
stopped, or beyond the 16 held. The library's own clipped pictures are left out while they are
|
|
587
|
+
taken: from the picture's call until 50 ms after its reply, by when a frame arrived or by its paint
|
|
588
|
+
time widened by the clock's uncertainty either side. A crop on the page's own session, as over CDP
|
|
589
|
+
and on Browserbase, is drawn at the crop's own size, so there only frames of another size are left
|
|
590
|
+
out, and the page's own keep reaching readers. Where Playwright emulates the viewport, as
|
|
591
|
+
`Chromium.layer` does, a crop keeps the device's size, so every frame in that window is left out,
|
|
592
|
+
the page's own included: the capture pauses for the picture's round trip, plus 50 ms, plus twice
|
|
593
|
+
the clock's uncertainty, about 100 ms a zoom locally, and a crop with the viewport's own
|
|
594
|
+
proportions never reaches readers either. A clip someone else takes passes where Chromium keeps
|
|
595
|
+
the device's size during it and the clip has the viewport's proportions, and on a page that paints
|
|
596
|
+
nothing after it, a clip of any shape lasts long enough to pass for a resize.
|
|
597
|
+
Each page's own session holds focus emulation, so a tab behind another keeps painting and its
|
|
598
|
+
capture keeps delivering, whatever else is attached; a capture starts once the browser has
|
|
599
|
+
confirmed it, and fails with `Timeout` at the action timeout if a renderer stuck in a script never
|
|
600
|
+
does.
|
|
601
|
+
A provider that reaches its browser over a network can run captures on a connection of their own,
|
|
602
|
+
a `Browser.CaptureSource` given as `Cdp.Options.capture`, as `effect-browserbase` does: there a
|
|
603
|
+
frame never waits behind a large message on the connection that drives the page. Frames then carry
|
|
604
|
+
the document their own connection saw commit, numbered as `Navigated` numbers them, and a failure
|
|
605
|
+
of that connection ends the capture with `Failed` and leaves the page as it was.
|
|
606
|
+
Concurrent readers share one native screencast and its quality and size: a reader without options
|
|
607
|
+
joins whatever is running, and one whose explicit options differ fails with `InvalidRequest` rather
|
|
608
|
+
than silently receiving other frames. Each reader has a bounded 16-frame queue; a slow reader's
|
|
609
|
+
observed sequence gaps add to `lost`. Late subscribers do not count earlier history.
|
|
610
|
+
ACKs are independent of reader speed and bounded to 32 unresolved replies; exhaustion ends capture
|
|
611
|
+
with a typed error. `recentFrames` keeps the frames painted within `frameHistory` of the newest,
|
|
612
|
+
5 seconds by default, a moment's default window. Capture stops when its last
|
|
613
|
+
reader leaves; a stop whose reply is late is never resent, and the next capture waits for it (up
|
|
614
|
+
to 2 s per attempt) rather than disabling capture for the page.
|
|
615
|
+
|
|
616
|
+
A page's `id` is its CDP target id. A new connection to the same browser, as after a dropped one,
|
|
617
|
+
finds each page under the id it had, with `browser.page(id)`; a stored id the browser no longer
|
|
618
|
+
finds is a page that closed. `browser.pages` lists the open pages in the order the browser began
|
|
619
|
+
tracking them, and `browser.firstPage` gives the first, opening one when there is none.
|
|
620
|
+
|
|
621
|
+
A page's life is told on the browser's timeline. `PageOpened`; `Navigated` for every move of its
|
|
622
|
+
main frame, `sameDocument` for one within the document, such as `pushState`, with `document`
|
|
623
|
+
counting the page's documents from 0, the one it had when the browser began tracking it;
|
|
624
|
+
`PageLoaded` as a document finishes parsing and loading, as the page's own session hears it, so
|
|
625
|
+
after the `Navigated` that began the document; `DialogShown`; and `PageClosed`, by the
|
|
626
|
+
page or because it crashed. A document count belongs to the page within one `Browser`, and starts
|
|
627
|
+
again on a new connection. Each frame carries the document it followed and the page's address. A
|
|
628
|
+
tab the site opened that the library could not track is `PageUntracked`. A tab a page opened names
|
|
629
|
+
it as its `opener`, and the opener's events hold its opening. Chromium announces a title change
|
|
630
|
+
only with the next change of address, so there is no title event: read `page.title` when
|
|
631
|
+
`Navigated` or `PageLoaded` says the page moved. The browser answers it, in one call that a busy
|
|
632
|
+
page cannot hold up, and an untitled page's is empty.
|
|
633
|
+
|
|
634
|
+
The browser's own end is one `Disconnected`, and `browser.disconnected` completes with its cause:
|
|
635
|
+
`connection`, `session`, at or after the provider's `expiresAt`, which `SessionEnding` announces,
|
|
636
|
+
or `released`, by the owner's scope. A browser lost so publishes no `PageClosed` for its pages,
|
|
637
|
+
and calls in flight on them fail at once rather than at their deadline. A screencast's readers are
|
|
638
|
+
told as the page's scope closes, once that cause is known: a capture that fails first, as one on a
|
|
639
|
+
connection of its own can, waits up to two seconds for it before it fails as itself. A crashed page
|
|
640
|
+
is closed: Playwright drives it no more, and its calls would never return.
|
|
641
|
+
|
|
642
|
+
A failure on a page that is gone is `Closed` with its cause: `page`, `crashed`, or the browser's.
|
|
643
|
+
`BrowserError.consequence(error)` says what any failure leaves: what was `lost` (`nothing`, the
|
|
644
|
+
`page`, or the `session` with its pages), and whether to `repeat` the call: `safe`, as nothing
|
|
645
|
+
reached the browser; `check` its effect first, as it may have taken place; `pointless` as it is,
|
|
646
|
+
such as with a stale ref; or `resume` reading events from a newer cursor. `message` is a sentence
|
|
647
|
+
for operators, and the browser tools tell a model what to do about the failure.
|
|
648
|
+
|
|
649
|
+
`Expect` is that check, for an action whose effect the page shows. An attempt names what the action
|
|
650
|
+
is for, `appeared(query)`, `gone(query)`, `changed(query)` or `navigated(url?)`, with `Page.find`'s
|
|
651
|
+
queries over the whole document, and takes its baseline as it begins. Each `run` checks first and
|
|
652
|
+
sends nothing where the expectation holds already, `AlreadyDone`; otherwise it acts and waits for
|
|
653
|
+
it, `Done`, or `NotDone` after `wait`, 5 seconds by default. An action that fails after its input
|
|
654
|
+
went is checked too: `Done` if its effect came, else its own failure, so the next `run` of the same
|
|
655
|
+
attempt, on the same page or the one a reconnect finds by its id, acts only if nothing came.
|
|
687
656
|
|
|
688
657
|
```ts
|
|
689
|
-
const
|
|
690
|
-
|
|
658
|
+
const placing = yield * Expect.attempt(page, Expect.appeared({ text: "Order placed" }));
|
|
659
|
+
const outcome = yield * placing.run(page, (page) => page.click(ref));
|
|
691
660
|
```
|
|
692
661
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
662
|
+
A check is one call to the page, and so is a wait, which the page answers as the effect comes;
|
|
663
|
+
`navigated` reads what the page already knows, at no call. `changed` compares what the query found
|
|
664
|
+
at the start, its elements' text, field values and states but focus, by a mark the page keeps, so
|
|
665
|
+
none of it leaves the page. On a page a reconnect found, `navigated` without an address tells only
|
|
666
|
+
a move to another address, as the new connection counts documents afresh. A presenter's view is
|
|
667
|
+
checked as its page is; a page a `Browser` did not make is refused.
|
|
668
|
+
|
|
669
|
+
Every address the library reports, in events, frames, reads, errors, a guard's request and the
|
|
670
|
+
outline's links, keeps its identity and loses its userinfo and its credentials' values, which read
|
|
671
|
+
`Page.redacted`. A credential is a query, fragment or path parameter named for a token, secret,
|
|
672
|
+
password, signature, assertion, session id or one-time code, or for a code or key that signs
|
|
673
|
+
someone in, such as `verification_code` or `api_key`. `code`, `key`, `session`, `sid` and `ticket`
|
|
674
|
+
as often name what a page shows, so under them only a value that looks generated, of at least 16
|
|
675
|
+
characters with letters and digits, is one. A chart's `?ticker=ETH` and a quote's `?code=BTC`
|
|
676
|
+
stay; a short one-time code under a bare `code` stays too.
|
|
677
|
+
|
|
678
|
+
`initScripts` take `{ match, source }`. The source runs before each new document's own scripts, in
|
|
679
|
+
every frame, or only where `match`, a `RegExp`, finds the document's address, as a block. Playwright
|
|
680
|
+
registers them as it sets each page up, so a popup's first document runs them too, at no call per
|
|
681
|
+
document.
|
|
682
|
+
|
|
683
|
+
`Browser.events()` streams `RecordedEvent` values: `{ sequence, event }`. The sequence orders
|
|
684
|
+
all browser events and is the replay cursor. Call `browser.events({ after: lastSequence })` to
|
|
685
|
+
resume, or use `after: 0` for everything since the browser opened if it is still retained. With
|
|
686
|
+
no cursor, streaming begins at subscription. `eventHistory` keeps a positive bounded number of
|
|
687
|
+
events (4,096 by default), also exposed without envelopes by `recentEvents`. A reader whose next
|
|
688
|
+
event has expired fails with `EventHistoryExpired`; it never skips events silently. Consume the
|
|
689
|
+
stream promptly and size retention for the consumer’s delay. Closing the browser scope wakes and
|
|
690
|
+
ends idle readers.
|
|
691
|
+
|
|
692
|
+
The presentation track uses the same channel. `TrackPlanned` publishes a whole glide before
|
|
693
|
+
input, with `from` and samples whose `afterMillis` are offsets from `at`. Its sequence identifies
|
|
694
|
+
the plan. `TrackPerformed` ends that plan, names it in `plan`, and records the number of samples
|
|
695
|
+
actually submitted plus the last submitted point. The consumer must trim the planned suffix to
|
|
696
|
+
that count after interruption or failure. `complete` means all samples were submitted; it does
|
|
697
|
+
not claim that the browser acknowledged or painted them.
|
|
698
|
+
|
|
699
|
+
`PointerPressed`, `PointerReleased`, `WheelScrolled`, `KeyChanged`, `TextInserted` and
|
|
700
|
+
`CursorChanged` describe the remaining input. Button, key and text events are published at
|
|
701
|
+
submission, including cleanup releases. Cursor shape comes from resolved target metadata.
|
|
702
|
+
`TrackEvent` is the schema union for these presentation events; a window leaves them out of its
|
|
703
|
+
events. Compositing remains the consumer’s job.
|
|
704
|
+
|
|
705
|
+
A page waits only for itself. Its operations take turns in one lane of its own: an action, which
|
|
706
|
+
sends input or navigates, has the page to itself, in the order actions were asked; reads (the
|
|
707
|
+
outline, `find`, `text`, `changes`, pictures, zooms, `title` and the viewport) share it, after the action in
|
|
708
|
+
flight and every action asked before them, so a read describes the page an action left, never one
|
|
709
|
+
an action is still changing. `ready` follows the action in flight too, and holds none back while it
|
|
710
|
+
watches. An action then waits for its page's unresolved input replies. Its timeout bounds those
|
|
711
|
+
waits before a full timeout bounds the action itself: a turn not given in time fails `Busy`, with
|
|
712
|
+
how long it waited and how many operations were ahead, and replies not answered in time fail
|
|
713
|
+
`Timeout`, as for a page that does not answer. `Page.failFast(effect)` runs operations that fail
|
|
714
|
+
`Busy` at once instead of waiting. A policy hold leaves the page's turn free.
|
|
715
|
+
|
|
716
|
+
`browser.suspend({ drain })` withholds library input on every tab for the current scope. It first
|
|
717
|
+
drains operations already admitted, through their `Action` records and native acknowledgements.
|
|
718
|
+
A canceled call may already have sent input: its unanswered reply still prevents a successful
|
|
719
|
+
drain. New and queued writes fail `Suspended`, with `dispatched: false`, and record that refusal.
|
|
720
|
+
Already-held authority refuses input before invoking an input guard; a guard already waiting is
|
|
721
|
+
checked again at admission. Reads, captures and events continue.
|
|
698
722
|
|
|
699
723
|
```ts
|
|
700
|
-
|
|
701
|
-
|
|
724
|
+
yield *
|
|
725
|
+
Effect.gen(function* () {
|
|
726
|
+
const paused = yield* browser.suspend();
|
|
727
|
+
// An event reader can resume from paused.since; paused.at is host monotonic time.
|
|
728
|
+
const page = yield* browser.firstPage;
|
|
729
|
+
yield* page.screenshot();
|
|
730
|
+
yield* operatorDone;
|
|
731
|
+
}).pipe(Effect.scoped);
|
|
732
|
+
// Input resumes after this scope, provided no other suspension still holds it.
|
|
702
733
|
```
|
|
703
734
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
735
|
+
Suspensions nest, including scopes closed out of order. `drain` defaults to `navigationTimeout`
|
|
736
|
+
(30 seconds); zero acquires only when input is already idle. Waiters that join a drain share its
|
|
737
|
+
first deadline. Expiry fails all those waiters with `Busy` and restores admission; cancellation
|
|
738
|
+
abandons only that caller's claim. Browser loss fails acquisition with `Closed`. A failed
|
|
739
|
+
acquisition leaves no suspension behind. The gate controls library actions and navigation;
|
|
740
|
+
input from the provider's own viewer or direct native calls remains under the operator's control.
|
|
741
|
+
The consumer owns the operator's decisions and any durable workflow journal.
|
|
742
|
+
|
|
743
|
+
Reads keep their work. An identical read asked while one is in flight on the page, with no action
|
|
744
|
+
asked between them, joins it, and costs no call of its own. Each read runs in the page's scope under
|
|
745
|
+
the action timeout whoever gives up, so a caller's own `Effect.timeout` loses nothing: a read that
|
|
746
|
+
ends after all its callers gave up serves the next caller to ask the same within an action timeout,
|
|
747
|
+
unless an action or a new document came first. An action stops the reads nobody awaits rather than
|
|
748
|
+
wait for them. Pictures and `changes` only join: a picture's caller says how old it may be, and a
|
|
749
|
+
window of changes ends as it is read.
|
|
750
|
+
|
|
751
|
+
`page.state` is what the library already knows of a page, at no call and with no wait for its
|
|
752
|
+
turn: where its main frame is, its document and when it was committed, how far that document has
|
|
753
|
+
loaded, its newest screencast frame, and the viewport's text and title as last read in that
|
|
754
|
+
document, each with when it was learned, so a caller reads their ages. A caller that must never
|
|
755
|
+
wait, such as one reading the screen on air, reads it rather than polling the page. Chromium
|
|
756
|
+
announces no title change, so the title is as a read of the text last found it.
|
|
757
|
+
|
|
758
|
+
`Browser.Options.maxPages` bounds the pages a browser keeps open, those a site opened included,
|
|
759
|
+
which are never refused: at the limit, `newPage` waits within the action timeout for one to close,
|
|
760
|
+
then fails `Limit`, or fails at once under `Page.failFast`.
|
|
761
|
+
|
|
762
|
+
Each page has its own pointer, where its last move left it, which is also where Chromium has it:
|
|
763
|
+
its first glide starts mid-viewport, and each later one where the one before ended, whatever
|
|
764
|
+
input on another tab does meanwhile. Every sent move updates the position, including a partially
|
|
765
|
+
cancelled glide. A later viewport clamps the starting point to its bounds if necessary.
|
|
766
|
+
|
|
767
|
+
## Supervising browser generations
|
|
768
|
+
|
|
769
|
+
`Supervisor.make` keeps a browser open across losses and session ends. Its `open` builds each
|
|
770
|
+
generation in a scope of its own and can return `{ browser, value }`: the application's pages,
|
|
771
|
+
tools or other resources belong to that generation. A caller that stops waiting never interrupts
|
|
772
|
+
the shared acquisition. Browser-only providers can continue returning `{ browser }`, with a
|
|
773
|
+
`void` value. Pages and values do not carry over when a new generation opens.
|
|
715
774
|
|
|
716
775
|
```ts
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
`destination` is resolved by the browser against the document's base URL, and honours a `formaction` override. It can carry a token, which is why it is not in an `Observation`. An over-long destination is left out, never cut. No value and no markup is ever included.
|
|
723
|
-
|
|
724
|
-
A reference is to the control that was inspected, not merely to a node. If the same attached node now has a different destination, input type, autocomplete category, form method, label or disabled state, acting on it fails `stale` and `undispatched` before input, exactly as it does for a replaced or detached node. A performed action also rechecks after delayed preparation; an acknowledged preparatory scroll or focus retains its `rejected` outcome if that later check fails. An existing reference is never substituted by a selector or label lookup. A text-entry control's label never comes from what was entered into it: a textarea, an editable element or a `textbox`, `searchbox`, `combobox` or `spinbutton` role is named only by `aria-label`, `placeholder`, its `<label>` or `aria-placeholder`, so typing neither renames it nor puts the typed text into a recorded descriptor.
|
|
776
|
+
import { Effect } from "effect";
|
|
777
|
+
import * as Chromium from "effect-browser/Chromium";
|
|
778
|
+
import * as Supervisor from "effect-browser/Supervisor";
|
|
725
779
|
|
|
726
|
-
|
|
780
|
+
const program = Effect.gen(function* () {
|
|
781
|
+
const browsers = yield* Supervisor.make({
|
|
782
|
+
open: Effect.gen(function* () {
|
|
783
|
+
const browser = yield* Chromium.open();
|
|
784
|
+
const page = yield* browser.firstPage;
|
|
727
785
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
page.fillElement(reference, value, {
|
|
731
|
-
admit: (facts) => facts.inputType !== "password" && facts.autocomplete !== "cc-number",
|
|
786
|
+
return { browser, value: page };
|
|
787
|
+
}),
|
|
732
788
|
});
|
|
789
|
+
return yield* browsers.use(({ value: page }) => page.snapshot());
|
|
790
|
+
});
|
|
733
791
|
```
|
|
734
792
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
`
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
`selectOption(reference, options, admission?)` selects once on the native `<select>` named by
|
|
742
|
-
an `ObservedElement`. The options are a nonempty array of at most 64 unique option `elementId`s
|
|
743
|
-
from the same observation. No label, value or selector is a substitute for an issued ID.
|
|
744
|
-
|
|
745
|
-
An observed native select has `multiple` and `optionsTruncated` metadata. Its retained option
|
|
746
|
-
controls carry `selectElementId`, `selected`, `disabled` and a bounded label. Choices of a visible
|
|
747
|
-
select are available in viewport observations even when its menu is collapsed; they consume the
|
|
748
|
-
same `maxControls` allowance as other controls. Truncation is explicit, and an option outside the
|
|
749
|
-
retained set cannot be selected by guessing its position or value.
|
|
793
|
+
`use(work, { wait })` atomically borrows the serving generation and runs `work` in a scope of its
|
|
794
|
+
own. The lease gives its `generation`, `browser`, `value`, an `ending` notice and `retain`.
|
|
795
|
+
`wait` defaults to `waitTimeout` (two minutes); zero either borrows immediately or fails
|
|
796
|
+
`Unavailable`. Cancelling the caller closes its work scope and lets its hold go. Ordinary work
|
|
797
|
+
errors stay ordinary work errors. `browser` still returns an unheld browser: use it only when the
|
|
798
|
+
caller can tolerate that generation being released underneath it.
|
|
750
799
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
each requested option through `revalidateElement`; revalidating only the select does not approve
|
|
755
|
-
its option nodes.
|
|
756
|
-
|
|
757
|
-
The owner validates the same select and option nodes, document, current membership, multiple and
|
|
758
|
-
enabled state before dispatch. It also compares the submitted value privately without returning
|
|
759
|
-
that value in the observation or action result. Duplicate labels remain distinguishable through
|
|
760
|
-
their IDs. Changed or replaced controls are refused before input; unknown outcomes after dispatch
|
|
761
|
-
are never replayed. Selection emits ordinary native select input/change events through the
|
|
762
|
-
maintained engine, returns `ActionResult` and retires the observation on that page. It does not
|
|
763
|
-
claim the website finished work triggered by those events. The same synchronous host `admission`
|
|
764
|
-
used for exact-node input applies to the selected control.
|
|
765
|
-
|
|
766
|
-
### Filling a form in one operation
|
|
767
|
-
|
|
768
|
-
`fillForm(request, admission?, options?)` sets several controls of one observation, in order, then optionally clicks one submit control:
|
|
800
|
+
A retained scope transfers a hold beyond the `use` call. Acquire anything that must travel with
|
|
801
|
+
that hold in its scope, and close it when the receiver is finished. Within a scoped program with
|
|
802
|
+
`browsers` as above and `Exit` and `Scope` imported from `effect`:
|
|
769
803
|
|
|
770
804
|
```ts
|
|
771
|
-
const
|
|
805
|
+
const prepared =
|
|
772
806
|
yield *
|
|
773
|
-
|
|
774
|
-
observationId: seen.observationId,
|
|
775
|
-
fields: [
|
|
776
|
-
{ elementId: email, value: "ada@example.test" },
|
|
777
|
-
{ elementId: terms, checked: true },
|
|
778
|
-
{ elementId: plan, options: [pro] },
|
|
779
|
-
],
|
|
780
|
-
submit: create,
|
|
781
|
-
});
|
|
782
|
-
// Dispatched toggle fields and the optional submit also carry bounded input receipts.
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
Each field gives exactly one of `value`, text that replaces the contents of an input, textarea or content-editable element; `checked`, the state a checkbox, radio or switch should end in, clicked only when it differs, and a native radio is never asked to clear itself; or `options`, issued option IDs of a native select exactly as for `selectOption`. A form has at most 32 fields, each control at most once, and its submit control is not also one of its fields.
|
|
786
|
-
|
|
787
|
-
Every step is its own admitted and charged action on the exact observed node, after the same fresh checks as `fillElement`, `selectOption` and `clickElement`, including the host's `admission` policy. Each step, the verification read included, captures its own action deadline and queue window when it is requested, so `timeoutMillis` and `admission.queue` bound every step rather than the whole form; inside a plan, the step's captured bounds still cap them all. One difference is deliberate: a control may have become enabled since it was observed, such as a submit button a form enables once it is complete, but it must be enabled when its step runs. The observation stays usable for this form's own steps only. Navigation, a page hold or any other caller's action still retires it, and the form retires it when it ends.
|
|
788
|
-
|
|
789
|
-
After each dispatched step, the page's own handlers get at least two rendered frames and `settleMillis` (50 by default, at most 5000, 0 to skip). A text field is then left, as a person moving on would leave it, so formatting a page applies on blur belongs to that step. Before submit, `verify` (true by default) reads every field again and stops the form when one no longer holds what its own step left there: a re-render, an asynchronous reset, anything that changed a field after it was set. What was read is compared on the host and never returned.
|
|
790
|
-
|
|
791
|
-
The first refused or uncertain step ends the form, and nothing after it is sent. A form is not a transaction: fields set before the stop stay set, and `stopped` says where it ended (`field`, `verify` or `submit`) and why, as a `BrowserError` whose outcome says whether that step itself was dispatched. A toggle the page would not change stops with `failed` and `rejected`. `submitted` is true only when the submit click completed. The operation fails outright only when its first step does, exactly as that single action would.
|
|
792
|
-
|
|
793
|
-
### Bounded waits that leave room for recording
|
|
794
|
-
|
|
795
|
-
`waitFor({ selector, state })` still supports the host's bounded selector conditions: `visible`,
|
|
796
|
-
`hidden`, `attached` and `detached`. It captures the issued page, frame and document when admitted.
|
|
797
|
-
`waitForElement({ reference, state, timeoutMillis? })` instead waits on an exact node issued by the
|
|
798
|
-
current observation, with `visible`, `hidden`, `enabled` or `disabled`. It never searches for a node
|
|
799
|
-
that replaced the reference. Hidden includes detachment of the original node; for the other
|
|
800
|
-
conditions detachment fails `Stale/undispatched`. Replacing the document or frame fails even a
|
|
801
|
-
hidden wait. A node's changed enabled/visible state is what the wait observes, not fresh input
|
|
802
|
-
authorization: inspect again before acting on changed control state.
|
|
803
|
-
|
|
804
|
-
Wait admission briefly holds the existing permit, checks readiness and page holds, and charges
|
|
805
|
-
one model-reachable action. The pending wait then owns its own cancellation signal, connection
|
|
806
|
-
generation and absolute deadline. An exact-node request may shorten the deadline to 1–60,000 ms;
|
|
807
|
-
the configured action timeout and remaining lifetime still cap it, including setup. Selection
|
|
808
|
-
can move elsewhere without retargeting the wait. Its condition is a sample, not a reservation
|
|
809
|
-
that the page will remain unchanged.
|
|
810
|
-
|
|
811
|
-
While a wait is pending, `checkpoint`, `listPages` and ordinary bounded reads can use the normal
|
|
812
|
-
permit. Mutations, another navigation and page holds on the waited page are refused before
|
|
813
|
-
dispatch. Input and observation on another page may proceed; replacing the waited frame's observation is refused until the logical wait ends. Closing a page or session remains available and cancels affected waits.
|
|
814
|
-
No general read-under-write bypass has been added.
|
|
815
|
-
|
|
816
|
-
One native wait may be outstanding per exact Page, with a finite session bound of 32. Cancellation and timeout release the
|
|
817
|
-
logical barrier without inventing an uncertain mutation, but retain native capacity until the
|
|
818
|
-
actual native promise and any required handle disposal settle, or that exact connection retires.
|
|
819
|
-
Consequently another wait can still receive `Busy/undispatched` after its predecessor's caller
|
|
820
|
-
has stopped waiting. Status `busy` includes that retained native capacity; it does not mean every
|
|
821
|
-
ordinary operation is blocked. A cancelled exact-node wait keeps only its leased node alive when
|
|
822
|
-
the observation is replaced, and its late cleanup cannot dispose a successor's references.
|
|
823
|
-
|
|
824
|
-
### Passive checkpoints for a recorder
|
|
825
|
-
|
|
826
|
-
`page.observe()` replaces that frame's current observation, so a recorder calling it on the same frame would retire the references an agent is about to use. `page.checkpoint()` is the passive path: viewport text, control facts and, when asked, a PNG of the viewport.
|
|
827
|
-
|
|
828
|
-
```ts
|
|
829
|
-
const checkpoint = yield * page.checkpoint({ picture: true });
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
It issues no references, is not a mutation, and leaves the action observation and the selection exactly as they were, so inspect, checkpoint, then act on the inspected node all compose. It is host-only, because it carries control facts. Text and picture are read one after the other, never atomically: the interval is on the host monotonic clock, and `documentChanged` says the document was replaced in between. A held page is refused `busy` rather than woken to be read: take the checkpoint before the hold and keep it.
|
|
833
|
-
|
|
834
|
-
`checkpoint` and `controlFacts` each consume one separate host-read allowance, configured by
|
|
835
|
-
`Chromium.layer({ maxHostReads })` or the corresponding provider Layer. The default is 10,000,
|
|
836
|
-
with an explicit integer bound of 1–1,000,000; invalid values are refused rather than clamped.
|
|
837
|
-
Exhaustion reports `Limit { dimension: "host-reads", maximum, observed } / undispatched` while the
|
|
838
|
-
owner stays open. These operations still obey the same lifetime, bytes, deadline and admission
|
|
839
|
-
concurrency bounds: a checkpoint can execute page script and is not unlimited free work.
|
|
840
|
-
`observe`, `readText`, `screenshot` and `waitFor` continue consuming the model-reachable action
|
|
841
|
-
allowance, which `status.actions` reports. Neither budget is a tool parameter, and `maxActions`
|
|
842
|
-
has not become mutations-only.
|
|
843
|
-
|
|
844
|
-
### Real pointer and wheel input
|
|
845
|
-
|
|
846
|
-
`pointerMove`, `hover` and `wheel` send the input a person's hardware would, so pages see trusted events, `:hover` applies, and the browser itself decides what is under the pointer. `scroll` stays what it was: script in the page, instantaneous, raising no wheel event. That difference is how a recording tells one from the other.
|
|
847
|
-
|
|
848
|
-
```ts
|
|
849
|
-
const handle = session.initialPage; // exact original Page authority
|
|
850
|
-
|
|
851
|
-
yield * handle.pointerMove(PointerMoveRequest.make({ to: { x: 140, y: 100 } }));
|
|
852
|
-
yield * handle.hover(HoverRequest.make({ selector: "#menu" }));
|
|
853
|
-
// A nested scroll container under the pointer scrolls, not the page.
|
|
854
|
-
const receipt =
|
|
855
|
-
yield * handle.wheel(WheelRequest.make({ deltaX: 0, deltaY: 240, at: { x: 420, y: 120 } }));
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
Coordinates are CSS pixels in the main frame's viewport. Each logical input is one action on its issued Page or Frame. Performed plans add bounded pacing and output-only glide evidence; viewers draw cursor artwork from projected evidence without injecting presentation DOM into the website.
|
|
859
|
-
|
|
860
|
-
`hover` places the pointer on one exact element where it is, by selector or by the node an observation named (`page.hoverElement`). It never scrolls to reach it, because that would hide a scripted scroll inside a native-input operation. If the pointer cannot be placed on the element (it is outside the viewport, has no area, or something covers it) the call fails `not-visible` and `undispatched`. A performed hover aims inside the part of the element its viewport and overflow ancestors leave visible, so it reaches a partly clipped element a plain hover reaches, and refuses only one with no visible part.
|
|
861
|
-
|
|
862
|
-
For a child-frame element, hover checks the commanded point through each ancestor
|
|
863
|
-
frame and then the exact node, including cross-origin documents. The receipt
|
|
864
|
-
still uses main-viewport CSS coordinates. An overlay or clipping ancestor refuses
|
|
865
|
-
input. Frame traversal is bounded at 32 levels, with bounded shadow/DOM ancestry
|
|
866
|
-
checks. Axis-aligned translation and positive scaling are supported; rotation,
|
|
867
|
-
perspective and other unsupported frame mappings are refused rather than guessed.
|
|
868
|
-
These reads and native input are separate operations in the browser: page script
|
|
869
|
-
can still change geometry between validation and dispatch.
|
|
870
|
-
|
|
871
|
-
An `InputReceipt` carries the target it was sent to, the position this owner commanded, and an interval on the same host monotonic clock that stamps `CapturedFrame.receivedMonotonicNanos`. Pointer moves, hovers and positioned wheels report their known point. A Playwright-managed click reports `position: null`: Playwright does not expose its chosen click point through the supported API, so the receipt does not guess. The click still uses Playwright's exact-node checks and native hit testing. Its unknown point invalidates the owner's remembered pointer position, so later key receipts also report `null` until a known pointer command places it again. The click interval brackets the supported Playwright call and may include actionability or navigation waits; Playwright does not expose the exact native dispatch instant. Input and pixels share one timeline, so a compositor can place known points on frames that show them. A wheel or click receipt does not claim the page finished work triggered by the input or that any frame shows it.
|
|
872
|
-
|
|
873
|
-
### Real key input
|
|
874
|
-
|
|
875
|
-
`fill` sets a field's value in one step: the page gets an `input` event and no `keydown`, `keypress` or `keyup`, so anything that reacts to keys behaves differently under a recorder than it does for a person. `press` and `type` send the strokes a keyboard would. Handlers see trusted key events, and the browser does what it does for a person: Tab moves focus and selects the field it lands in, Enter submits a form that has a submit button, and Backspace edits.
|
|
876
|
-
|
|
877
|
-
```ts
|
|
878
|
-
const handle = session.initialPage; // exact original Page authority
|
|
879
|
-
|
|
880
|
-
// Focus is the page's business. A real click gives it, and keys then follow it.
|
|
881
|
-
yield * handle.click(ClickRequest.make({ selector: "#from" }));
|
|
882
|
-
yield * handle.type(TypeRequest.make({ text: "Vienna" }));
|
|
883
|
-
yield * handle.press(PressRequest.make({ key: "Backspace" }));
|
|
884
|
-
yield * handle.press(PressRequest.make({ key: "k", modifiers: ["Control"] }));
|
|
885
|
-
// Sent only if `#from` still has focus; otherwise nothing is sent at all.
|
|
886
|
-
yield * handle.press(PressRequest.make({ key: "Enter", into: "#from" }));
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
Keys go to whatever has focus in the issued page, in whichever frame that is, because that is where the browser sends them. `into` narrows that to one exact element, which must already have focus or hold the element that does, through any open shadow root. If it does not, the call fails `not-focused` and `undispatched`. It never focuses the element for you, for the reason `hover` never scrolls: that would hide a scripted focus inside a native-input operation. `page.pressElement` and `page.typeElement` apply the same rule to the node an observation named and take the same host admission as `fillElement`, so switching from `fill` to real typing gives up neither exactness nor the check on fresh control facts.
|
|
890
|
-
|
|
891
|
-
A key is spelled as the `KeyboardEvent.key` the page will see, and the vocabulary is closed: `Enter`, `Tab`, `Backspace`, `Delete`, `Escape`, the four arrows, `Home`, `End`, `PageUp`, `PageDown`, or one printable ASCII character (a space is `" "`), with `Shift`, `Control`, `Alt` and `Meta` as modifiers. The native engine parses a key string, chords included, and begins holding the modifiers before it has validated the key, so nothing reaches it that was not reviewed here. A modifier other than Shift makes a chord rather than a character, and nothing is typed.
|
|
892
|
-
|
|
893
|
-
`type` sends up to 256 characters as one charged action under a single action timeout. It submits ordered native input in windows of at most 16 code points and 32 commands, without waiting for every individual reply. Each normal stroke ends at key-up, and the window drains before another begins. The owner is checked before every command, including key-up: a fence stops unsent input, and no interrupted run is replayed or repaired. Failure while dispatched input is still unacknowledged leaves an `unknown` outcome; the owner closes that exact page, or fences the session when closure is unconfirmed. Failure after all earlier input was acknowledged reports `performed`. Neither result replays the run. A canceled caller does not release native typing capacity; unresolved attachment, replies or failed port cleanup retain it until confirmed retirement. A run whose replies all succeeded leaves its private port attached for the next run on the same Page. Each Page owns its port, with at most 32 retained ports across the connection; switching Pages does not detach another Page's idle port. Positive Page closure or connection retirement releases its capacity.
|
|
894
|
-
|
|
895
|
-
With `into`, the original node and document must still have focus before each subsequent window. Focus is never repaired. Commands already submitted in a window can land after focus moves. Once that window has successfully drained, a later `not-focused` refusal reports `performed`: earlier input was acknowledged, and the refused window sends nothing.
|
|
896
|
-
|
|
897
|
-
Performed typing checks focus once before each stroke, never between a key and its release: a
|
|
898
|
-
key whose own default action moves focus (Tab, an Enter that submits, an auto-advancing field)
|
|
899
|
-
still completes its stroke. A fully acknowledged stroke, including its key and modifier releases,
|
|
900
|
-
retains `performed` evidence if a later stroke is refused. An acknowledged text insertion, the
|
|
901
|
-
Backspace that erases a performed Fill's old value, a filled form field and an acknowledged
|
|
902
|
-
scroll sample do the same. Only focus, selection and scroll-into-view preparation remain partial
|
|
903
|
-
and report `rejected` when nothing else landed; unresolved key input still reports `unknown` and
|
|
904
|
-
receives the original containment policy. A later failure never weakens what was acknowledged.
|
|
905
|
-
|
|
906
|
-
A character the US layout cannot produce is committed as text, the way an input method commits it: the field changes and no key event says so. Plain `type` sends a shifted character as its own key with `shiftKey` false. When a page reads the modifier, send that stroke through `press` with `Shift` held, spelling the key as the page will see it: `{ key: "A", modifiers: ["Shift"] }`. Spelled `"a"`, the engine sends `a` with Shift down, which is what Shift produces with Caps Lock on. Control characters are refused in text because the engine presses Enter for a line break; a named key is always its own `press`. Performed plans hold Shift for uppercase and shifted punctuation and pace complete balanced strokes under one logical action. A stroke remains unresolved until its key and modifiers are released; quiet intervals do not renew the absolute deadline or retain unresolved native replies.
|
|
907
|
-
|
|
908
|
-
Like a plain window, a performed stroke submits its modifiers, its key and their releases in order
|
|
909
|
-
without waiting for each reply, so its key-up is submitted its planned hold after its key-down
|
|
910
|
-
rather than a round trip later, and a held Shift adds no round trip of its own. Delivery jitter
|
|
911
|
-
on the way to a remote browser is not controlled. The owner is still checked before every
|
|
912
|
-
command, nothing more is sent once a reply has failed, and the stroke drains all of its at most ten
|
|
913
|
-
replies before it resolves. Each stroke therefore still costs its releases' round trip, and its
|
|
914
|
-
focus check's when it names an element: against a remote browser, a stroke whose planned interval
|
|
915
|
-
is shorter than those starts late. A slow renderer still handles a stroke's events one at a time,
|
|
916
|
-
so the deadline check charges each stroke what strokes with as many commands have taken, scaled up
|
|
917
|
-
from smaller strokes until one of its size has drained.
|
|
918
|
-
|
|
919
|
-
A press waits for native input acknowledgement, without waiting for resulting navigation. If Enter submits a form, wait for what the next document shows with `waitFor`. A receipt carries the same target, pointer position and interval as any other input, and never says which key was pressed or what was typed. Typing a secret is still more observable than one `fill`, because the page sees every stroke; prefer `fill` for one unless the page requires keys. Both operations are available in the model-facing toolkit in `effect-agent-browser`.
|
|
920
|
-
|
|
921
|
-
The connection endpoint is read through the exact allocated session, so a provider reply that names a different session is refused before any CDP attachment.
|
|
922
|
-
|
|
923
|
-
Persistent Browserbase contexts require a live writer permit from `ContextCoordination.withWriter` when writes are persisted. Detach/reconnect is opt-in with `keepAlive`. Detach first lets admitted work finish within a bounded interval, such as a page close admitted behind its barrier or a started navigation that is still loading, rather than disconnecting beneath it; work that cannot finish fails the detach as `Timeout/undispatched` and leaves the session open. Reconnect creates a new handle generation, verifies the selected target, obtains fresh state, and never replays pending input or treats serialized agent state as a live browser.
|
|
924
|
-
|
|
925
|
-
Human handoff blocks new admission and drains native work that can still change a page within a
|
|
926
|
-
bounded interval before pausing automation and returning host-only Live View material. Pure waits
|
|
927
|
-
and reads do not hold it up; the pause refuses them as `Stale`. Work that cannot drain in time,
|
|
928
|
-
such as a navigation still loading, fails the request with `Timeout/undispatched`: no handoff is
|
|
929
|
-
granted, the barrier lifts and automation continues. Resume requires an explicit operator-release signal
|
|
930
|
-
and returns fresh bounded Page inventory under the lifecycle permit; observing a newly acquired
|
|
931
|
-
Page is explicit. Every Page and Frame issued before the handoff, `initialPage` included, stays retired after resume, though the connection is the same: its operations fail `Stale` and `undispatched`, so issue Pages again from `inventory.pages` with `session.page(info)`. Once the pause is installed, a failed Live View authorization does not silently resume automation. Live View URLs are temporary bearer material; iframe styling is not an authorization boundary. Live View is also where browser-window presentation already exists for watching a session as it runs, and it is the provider's: beside each full-screen URL Browserbase issues a bordered one (`debuggerUrl`, "mimic a real browser with borders"), and a navbar that `navbar=false` hides. This package decodes and returns only the full-screen URL. The bordered one carries the same control authority and would be issued under the same rules, so surfacing it is a small host-only addition whenever something needs it; nothing here does yet, so it is not exported.
|
|
932
|
-
|
|
933
|
-
## Registrations, capabilities and document readiness
|
|
934
|
-
|
|
935
|
-
`browser.launch(policy, { bootstrap })`, `browser.acquire(policy, { bootstrap })` and `browser.attach(endpoint, { policy, bootstrap })` take a trusted registration plan at acquisition. The browser Layer fixes launch configuration; consumer callback dependencies are captured at acquisition. A plan is built from `Bootstrap.binding`, `Bootstrap.init`, `Bootstrap.permissions` and `Bootstrap.combine`. Combination preserves the error and service unions of different handlers; the callable bridge precedes all dependent init steps in one native script registration. Script content, handler implementations and origin grants are host configuration, never model output or page input.
|
|
936
|
-
|
|
937
|
-
```ts
|
|
938
|
-
import * as Bootstrap from "effect-browser/bootstrap";
|
|
939
|
-
import * as Browser from "effect-browser/browser";
|
|
940
|
-
import { Chromium } from "effect-browser/chromium";
|
|
941
|
-
import { BrowserPolicy } from "effect-browser/browser-data";
|
|
942
|
-
import { Context, Effect, Schema } from "effect";
|
|
943
|
-
|
|
944
|
-
class ShowSettings extends Context.Service<ShowSettings, { readonly title: string }>()(
|
|
945
|
-
"ShowSettings",
|
|
946
|
-
) {}
|
|
947
|
-
|
|
948
|
-
const bootstrap = Bootstrap.combine(
|
|
949
|
-
Bootstrap.permissions({ origin: "https://portal.example.com", permissions: ["clipboard-read"] }),
|
|
950
|
-
Bootstrap.binding({
|
|
951
|
-
name: "getShowSettings",
|
|
952
|
-
origins: ["https://portal.example.com"],
|
|
953
|
-
input: Schema.Struct({ version: Schema.Literal(3) }),
|
|
954
|
-
output: Schema.Struct({ title: Schema.String }),
|
|
955
|
-
maxConcurrent: 2,
|
|
956
|
-
maxInputBytes: 256,
|
|
957
|
-
maxOutputBytes: 4096,
|
|
958
|
-
timeoutMillis: 2000,
|
|
959
|
-
failureMode: "fail-session",
|
|
960
|
-
handle: () => Effect.map(ShowSettings, (settings) => ({ title: settings.title })),
|
|
961
|
-
}),
|
|
962
|
-
Bootstrap.init({
|
|
963
|
-
id: "show-settings-v3",
|
|
964
|
-
origins: ["https://portal.example.com"],
|
|
965
|
-
content: "globalThis.__ready = globalThis.getShowSettings({ version: 3 }).then(() => true);",
|
|
966
|
-
readiness: {
|
|
967
|
-
expression: "globalThis.__ready",
|
|
968
|
-
timeoutMillis: 5_000,
|
|
969
|
-
existingDocuments: "RequireFreshNavigation",
|
|
970
|
-
},
|
|
971
|
-
}),
|
|
972
|
-
);
|
|
973
|
-
|
|
974
|
-
const run = Browser.scoped(
|
|
975
|
-
Chromium.launch(BrowserPolicy.unrestricted(), { bootstrap }),
|
|
976
|
-
(session) =>
|
|
807
|
+
browsers.use((lease) =>
|
|
977
808
|
Effect.gen(function* () {
|
|
978
|
-
yield*
|
|
979
|
-
yield*
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
);
|
|
983
|
-
// run requires Chromium and ShowSettings. Browser.scoped discharges both the
|
|
984
|
-
// callback's and the use function's Scope, but neither one's other services or errors.
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
Registration is installed before this connection creates any document, and permissions precede the bundle. It is still not readiness: an asynchronous step cannot pause a website's own scripts, so a document is ready only when its expression resolves to exactly `true`. Readiness is keyed by frame and document epoch and evaluated once per document, so a completed wait can never ready the document that replaced the one it observed.
|
|
988
|
-
|
|
989
|
-
Each binding accepts exactly one JSON-compatible argument and returns the output codec's **encoded** JSON value. A transforming codec such as `Schema.FiniteFromString` therefore exposes a string to the page while its host handler works with a number. The native callback validates the actual caller's allowed origin and document before input decoding, immediately before invoking the handler, and before replying. This uses Chromium's execution-context identity and `uniqueContextId` on child CDP sessions belonging to the existing connection: a frame URL, a page-supplied origin, and a reused numeric context id are not authorization. The pinned Playwright `exposeBinding` callback supplies a frame but not the calling document's identity; it is deliberately not used as a weaker substitute. No raw protocol or second browser owner is exposed.
|
|
990
|
-
|
|
991
|
-
Plans admit at most 16 uniquely named bindings. Omitted binding options default to `maxConcurrent: 1`, `maxInputBytes: 65536`, `maxOutputBytes: 65536`, `timeoutMillis: 10000` and `failureMode: "reject-call"`. Name, exact origins, codecs and handler remain required. Explicit bounds/mode pass the same validated registration path; zero, null or excessive values are rejected, never clamped. `combine` selects the most conservative `existingDocuments` policy across its scripts, even when their origin sets differ. Admission reserves capacity before native validation, codec work or a callback fiber starts; a timed-out native operation retains that reservation until it actually settles, including across reconnect. The page wrapper additionally rejects cyclic, sparse, accessor-bearing, non-plain, non-finite and non-JSON input rather than silently changing it through `JSON.stringify`. Its traversal admits at most 64 levels and 65,536 nodes; the configured byte limit still applies. Native target and default-document registries are finite, and closed native targets retire their authority immediately.
|
|
992
|
-
|
|
993
|
-
`reject-call` rejects only the affected invocation and permits subsequent healthy calls. `fail-session` completes `session.failure` with the original typed consumer cause and fences the owner. A call from a document outside the binding's origins is refused before admission under either mode: it takes no capacity, never reaches the codecs or the handler, and cannot fail the session. It is counted as rejected and recorded as a `reject-call` failure with reason `origin`. Pages receive only `BrowserBindingError: Browser binding call rejected`, with no host stack, consumer error payload, credentials or SDK cause. `session.bindingDiagnostics` is a bounded **host-only** snapshot containing per-binding accounting and the latest 32 typed causes; do not serialize it into a Tool response. Callback service reads run independently of a browser mutation. Reentrant browser work on that same Page follows ordinary admission: it fails `busy` and `undispatched` by default, or waits under an explicit finite queue. Other healthy Pages can proceed independently.
|
|
809
|
+
const hold = yield* lease.retain;
|
|
810
|
+
const page = yield* Effect.acquireRelease(lease.browser.newPage(), (page) =>
|
|
811
|
+
page.close.pipe(Effect.ignore),
|
|
812
|
+
).pipe(Scope.provide(hold));
|
|
994
813
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
Operations that depend on an initialized document wait for the current one. Navigation, selection and page management do not, so initialization cannot deadlock the navigation that produces the document it is waiting for. A document that was already running when the bundle was registered — the page you attach to, or the one a reconnect finds — never ran it: `RequireFreshNavigation` reports `RequiresNavigation` and refuses dependent work, while `AcceptAlreadyRunning` verifies the requirement against that document instead of assuming it. Neither reloads a page whose work may be uncertain; that stays your decision. `session.initialPage.ready()` reports the current document without charging an action, and an origin outside the plan is reported as `NotApplicable` rather than waited on.
|
|
998
|
-
|
|
999
|
-
The reviewed permission subset is exercised against real Chromium. Provider extensions, persistent contexts and provider reconnect evidence belong to the supplying integration; see the [Browserbase guide](../browserbase/README.md).
|
|
1000
|
-
|
|
1001
|
-
## Live capture and presentation
|
|
1002
|
-
|
|
1003
|
-
For a frame-processing pipeline, use `Capture.stream(page, options)`. It starts only when consumed and owns one interval per subscription. `Stream.take`, consumer failure and interruption all finish that interval before the stream completes, while the enclosing browser stays open. A new subscription creates a new interval; concurrent subscriptions on the same page still fail `busy` under the existing reservation. Capture bounds and native-stop quarantine rules are unchanged.
|
|
1004
|
-
|
|
1005
|
-
```ts
|
|
1006
|
-
import { Stream } from "effect";
|
|
1007
|
-
import * as Capture from "effect-browser/capture";
|
|
1008
|
-
|
|
1009
|
-
const firstFrame = Capture.stream(page, { lifetime: "page" }).pipe(
|
|
1010
|
-
Stream.take(1),
|
|
1011
|
-
Stream.runCollect,
|
|
1012
|
-
);
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
Use `Capture.start` instead when the host needs passive snapshots, an explicit stop acknowledgement or the final loss summary. Stream completion runs cleanup but does not itself assert that Chromium acknowledged the stop; unconfirmed cleanup keeps the page quarantined. Both APIs use the same bounded frame stream and owner.
|
|
1016
|
-
|
|
1017
|
-
Live capture frames carry owned JPEG bytes, captured target identity, sequence number, source presentation time, host monotonic receipt time, geometry and explicit drop accounting. Buffers are bounded by frame count and bytes; slow consumers drop old frames instead of creating an unbounded fiber/callback backlog. Buffer dropping is not page-clock backpressure and does not reduce what the browser produced upstream. Holding a capture callback is not a promise that page timers or animations stop.
|
|
1018
|
-
|
|
1019
|
-
`sourceTimeMillis` is the browser's wall clock when it took the frame for the screencast, stamped before the frame is encoded. Chromium encodes up to three frames at once and emits each when its encode completes, so two frames stamped close together can arrive in either order. A frame that arrives behind a newer one can no longer be presented in order: it is discarded and counted in `late`. It is never sorted back in or given another time, so delivered source times strictly increase and a gap in `sequence` marks the omission. Concurrent encoding can put at most two late frames in a row. A longer run means source time itself went backwards, and the interval ends with `Timestamp` error evidence.
|
|
1020
|
-
|
|
1021
|
-
The migration equation is **`discarded = overflow + late + duplicates + rejected`**. The four
|
|
1022
|
-
components are mutually exclusive: `overflow` is buffer eviction, `late` is out-of-order input,
|
|
1023
|
-
`duplicates` is repeated source time, and `rejected` contains other refused or undelivered frames.
|
|
1024
|
-
The old `dropped` field is removed, not redefined as overflow. A late frame was omitted from
|
|
1025
|
-
delivery; that fact does not prove network loss. The same counters appear in live snapshots,
|
|
1026
|
-
and `upstreamDrops` remains `"unknown"`. The default frame capacity remains four.
|
|
1027
|
-
|
|
1028
|
-
### Watching live or with a delay
|
|
1029
|
-
|
|
1030
|
-
A viewer can watch as it happens, or a few seconds behind so that text or other media made about a moment is ready when that moment is shown. Both are the same interval. For a delay, the consumer waits until each frame's `receivedMonotonicNanos` plus the delay before passing it on. While it waits, frames stay in the interval's own buffer, so `maxFrames` (at most 1024) and `maxBufferedBytes` are the delay's memory bound, and anything beyond them is dropped oldest-first and counted as `overflow`. Size them for the delay: five seconds of a busy page at up to 60 frames a second is 300 frames, and a 1280×720 JPEG of dense text is about 200 KB. The same clock stamps `documentBoundaries`, so an address bar drawn beside the frames changes when the frame after a boundary is shown. The record keeps the latest 64 boundaries, so a long stream can still name what it is showing. An interval may last up to six hours (`maxDurationMillis`, 60 seconds by default).
|
|
1031
|
-
|
|
1032
|
-
```ts
|
|
1033
|
-
import { Clock, Effect, Stream } from "effect";
|
|
1034
|
-
|
|
1035
|
-
const interval =
|
|
1036
|
-
yield *
|
|
1037
|
-
Capture.start(page, {
|
|
1038
|
-
lifetime: "page",
|
|
1039
|
-
size: { width: 1280, height: 720 },
|
|
1040
|
-
maxFrames: 400,
|
|
1041
|
-
maxBufferedBytes: 64 * 1024 * 1024,
|
|
1042
|
-
maxDurationMillis: 30 * 60_000,
|
|
1043
|
-
});
|
|
1044
|
-
const delayed = interval.frames.pipe(
|
|
1045
|
-
Stream.mapEffect((frame) =>
|
|
1046
|
-
Effect.flatMap(Clock.monotonicTimeNanos, (now) => {
|
|
1047
|
-
const wait = Number(frame.receivedMonotonicNanos + 5_000_000_000n - now) / 1e6;
|
|
1048
|
-
return Effect.as(wait > 0 ? Effect.sleep(wait) : Effect.void, frame);
|
|
814
|
+
return { page, hold, ending: lease.ending };
|
|
1049
815
|
}),
|
|
1050
|
-
)
|
|
1051
|
-
|
|
816
|
+
);
|
|
817
|
+
// Hand `prepared` to its next owner; that owner eventually lets it go.
|
|
818
|
+
yield * Scope.close(prepared.hold, Exit.void);
|
|
1052
819
|
```
|
|
1053
820
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
821
|
+
Rotation publishes `Draining` for the outgoing generation. It stays alive until its last hold
|
|
822
|
+
closes or `handover` expires, 30 seconds by default. `ending` is the first notice that it stopped
|
|
823
|
+
serving, with a `Superseded` reason and monotonic `revokeAt`; loss or retirement can revoke sooner.
|
|
824
|
+
At revocation, `use` interrupts work still running, waits for its finalizers and fails
|
|
825
|
+
`Superseded`. New `retain` calls refuse a revoked or releasing generation, and a retained scope
|
|
826
|
+
cannot extend the deadline. Retained scopes and work scopes close before the provider's
|
|
827
|
+
`release`; the generation's own scope closes last. Cleanup can therefore extend the time taken to
|
|
828
|
+
finish revocation. `Superseded` does not say whether an input reached the site: use the action's
|
|
829
|
+
receipt and plan stopping point to resolve that uncertainty.
|
|
830
|
+
|
|
831
|
+
Nonexclusive `rotate`, including `rotateBefore` timed rotation, opens the next generation while
|
|
832
|
+
old holds drain. Handover does not delay later rotations; multiple old generations can remain
|
|
833
|
+
owned, so the consumer chooses its rotation and overlap budget. Calls share one acquisition in
|
|
834
|
+
progress. Under `exclusive`, the current generation drains and finishes its provider release
|
|
835
|
+
before acquisition starts. Call `rotate` outside `use` for a clean cut: an exclusive rotation
|
|
836
|
+
inside `use` waits for its own hold to end or be revoked.
|
|
837
|
+
|
|
838
|
+
A loss publishes `Lost` with the browser's `disconnected` cause and revokes its holds immediately.
|
|
839
|
+
It reopens on `reopen`, which also retries an open failure until the schedule ends. A provider's
|
|
840
|
+
`definite` failure goes `Down` at once, with the cause also available through `Unavailable`.
|
|
841
|
+
`retire` revokes immediately, stops acquisition and waits for every generation to release.
|
|
842
|
+
Closing the supervisor's scope does the same unless `keep` is set: then only the serving
|
|
843
|
+
generation skips provider release, `Kept`; its application scopes still close. Other generations
|
|
844
|
+
are released normally.
|
|
845
|
+
|
|
846
|
+
`states` streams `Opening`, `Reopening`, `Open`, `Draining`, `Lost`, `Down`, `Closed` and `Kept`.
|
|
847
|
+
`Closed.released` preserves `Settled` or `Unconfirmed`; the latter does not establish that a
|
|
848
|
+
provider stopped the browser or its billing. `Generation.at` is wall-clock epoch milliseconds;
|
|
849
|
+
`hostTime` and `revokeAt` are host monotonic milliseconds from the supervisor's Effect `Clock`.
|
|
850
|
+
They can be compared with browser event and frame times when those use that same clock and host,
|
|
851
|
+
not across processes or against a provider's wall-clock timestamps. A generation event is
|
|
852
|
+
lifetime evidence, not proof that a frame was shown. `Browserbase.supervise` supplies hosted
|
|
853
|
+
generations and, with `keep`, can adopt one it previously kept.
|
|
854
|
+
|
|
855
|
+
Every module is also an entry point, such as `effect-browser/Page`. The
|
|
856
|
+
[repository README](https://github.com/mannyc2/effect-agent-browserbase#readme) has examples.
|
|
857
|
+
|
|
858
|
+
## Tracing
|
|
859
|
+
|
|
860
|
+
Operations are spans in Effect's tracer, so an application that installs an exporter sees them in
|
|
861
|
+
its own traces; the library installs none. For the agent in the repository's README, with
|
|
862
|
+
`effect/observability` provided outermost so that opening the browser is traced too:
|
|
1061
863
|
|
|
1062
864
|
```ts
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
```
|
|
1067
|
-
|
|
1068
|
-
`snapshot` copies the bounded metadata already recorded in host memory. It does
|
|
1069
|
-
no browser work, consumes no frames, charges no action, and works while the page
|
|
1070
|
-
is held. Repeated reads neither stop nor restart capture. `observedMonotonicNanos`
|
|
1071
|
-
stamps the read; each boundary retains its own commit observation time. The
|
|
1072
|
-
record keeps the latest 64 boundaries; `documentBoundariesTruncated` reports that earlier
|
|
1073
|
-
ones were let go, and `currentDocument` continues counting. Addresses longer than the existing
|
|
1074
|
-
bound remain `null`. The model should not receive these host-only addresses by
|
|
1075
|
-
accident merely because it can inspect the page.
|
|
1076
|
-
|
|
1077
|
-
Every summary and snapshot includes `qualification`: this exact target's authority phase,
|
|
1078
|
-
containment facts, and the owner's phase and generation. A revoked Page may still be awaiting
|
|
1079
|
-
closure; `PageClosed` appears only after positive native closure, independently of `nativeStop`.
|
|
1080
|
-
Later `snapshot` and `completed` reads can retain that confirmation even when capture ended
|
|
1081
|
-
earlier. The original action outcome and capture end reason remain unchanged. A terminal Page
|
|
1082
|
-
requires explicitly fresh authority and intent before another capture can start.
|
|
1083
|
-
|
|
1084
|
-
While capturing, `reason` and `nativeStop` are `null`. `phase: "stopping"` means
|
|
1085
|
-
capture has stopped accepting frames but native cleanup is pending. `"stopped"`
|
|
1086
|
-
means that cleanup attempt settled; only `nativeStop: "confirmed"` confirms the
|
|
1087
|
-
native stop. All snapshots are observations of accounting so far: buffered frames
|
|
1088
|
-
can still be drained after stop. For final accounting, stop or await capture
|
|
1089
|
-
termination, finish draining `frames`, then read `completed` to obtain the terminal
|
|
1090
|
-
summary with final delivery counts. Preserve the terminal error and loss counters
|
|
1091
|
-
when reconciling earlier snapshots. `late` is one disjoint component of `discarded`, and
|
|
1092
|
-
`upstreamDrops` remains `"unknown"`; live metadata adds no stronger pixel or loss
|
|
1093
|
-
guarantee.
|
|
1094
|
-
|
|
1095
|
-
### What a compositor is given, and what it owns
|
|
1096
|
-
|
|
1097
|
-
Footage from `capture` is the page surface. It has no pointer, no tab strip and no address bar, so on its own it reads as the inside of a tab rather than as a browser. Drawing those is the application's, exactly as cursor artwork, easing and encoding are: this package has no window-compositing API and will not grow one. What it owes a compositor is the evidence only it can see, on one timeline:
|
|
1098
|
-
|
|
1099
|
-
- each frame's `receivedMonotonicNanos`, and the `document` it was received during;
|
|
1100
|
-
- an `InputReceipt` for each exposed native pointer, click and key operation, with a known position when the owner has one and an interval on that same clock. Internal download and file-chooser clicks clear the remembered pointer position but do not expose a receipt;
|
|
1101
|
-
- an address for every document a frame can name: `initialUrl` for document 0, read in the same turn the watch is installed, and a `url` on each cross-document boundary, read inside the navigation event that committed it. Page-lifetime captures separately mark same-document URL changes without advancing the document number. An application that samples `observe()` between actions can learn an address, but never when it became the address. One longer than 8192 characters is `null`, never cut into an address the page did not show.
|
|
1102
|
-
|
|
1103
|
-
A cross-document boundary is the commit, and it is the only moment of a navigation an application cannot see for itself. A same-document boundary records a URL change without claiming that a new document committed. When a cross-document navigation started and when its document finished loading are yours to stamp around the operation that caused it, because you made the call: `startNavigation` returns at dispatch and its `completed` resolves at DOMContentLoaded. Frames and receipts use the session's captured Effect `Clock`; a later caller Clock override does not change their domain. Capture `yield* Clock.Clock` when opening the session if you need raw host stamps on that same clock. `session.timeline.now` supplies the qualified offsets used by journal intervals. A title is page state that changes whenever the page likes, not part of a transition: read it with `page.describe()` when you need one, and stamp that read yourself.
|
|
1104
|
-
|
|
1105
|
-
The two clocks are never related for you. `sourceTimeMillis` is the browser's wall clock; everything above is the host's monotonic clock; on a hosted session they differ by an offset this package cannot observe. Every frame it receives is already late by the very capture latency it would be trying to measure, and nothing else it is handed carries the browser's time. Relating them takes a round trip into the page, which costs either a charged action or a registered binding, on a schedule only the application can choose, and it fails on a held page. So place input on frames by receipt time, which is always available and late by the capture latency, or measure the offset yourself with a four-timestamp exchange over a typed binding, as NTP does: the page stamps when it called and when the reply arrived, the host stamps when it received and when it replied, and half the best round trip is the error bound to report beside the offset.
|
|
1106
|
-
|
|
1107
|
-
## Explicit stage-page holds (opt-in)
|
|
1108
|
-
|
|
1109
|
-
Set `pageControl: true` on `Chromium.layer` or `BrowserbaseBrowser.layer` to use the host-only `page-control` module. The default remains off. `PageControl.suspend(page)` returns a live `PageSuspension`; `PageControl.resume(page, receipt)` consumes that exact receipt. Use an issued Page from `session.page(info)` or `initialPage`. Selection can move to the scout without invalidating the receipt, but connection loss, external target invalidation, completed resume, or another session does invalidate it. Holding or resuming a page may run its `freeze` and `resume` handlers, so nothing observed on _that_ page may be acted on unchecked afterwards: a reference fails `stale` until `page.revalidateElement(reference)` confirms it is still attached and still the control that was inspected. That check sends nothing, never searches for a substitute, and refuses a replaced, detached or changed node. An observation of another page is untouched, so an agent keeps driving the scout while the stage is held. `PageControl.state` reports the last acknowledged local state, not proof about a lost remote connection.
|
|
1110
|
-
|
|
1111
|
-
This opt-in uses maintained CDP attachment with `noDefaults: true` and owner-controlled per-page focus emulation. It intentionally does not support `keepAlive`, reattachment, human handoff, or popup/dialog `pause` policies. These combinations fail before acquisition; use the existing `retain`/`close` popup policies and `dismiss` dialog policy. Resume explicitly activates the native page without changing SDK selection. Do not enable it where another native client owns focus. Modeled input, DOM reads, waits and viewport changes on held/unknown pages fail before dispatch; the scout remains operable. Page close and session close remain available. Capture does not thaw a held page; frame consumption and acknowledgements never suspend/resume it implicitly.
|
|
1112
|
-
|
|
1113
|
-
The native tests cover page timers, RAF and CSS animation, scout progress with both pages captured, an already-paused animation, and restoration of a non-default rate. Resume waits for one bounded real RAF in an isolated world at rate zero before restoring that rate, avoiding Blink's stale pre-hold animation clock. In-flight callbacks are not undone. Date/wall time, network, media/audio, workers/service workers, and external/provider actions are not promised frozen. This is presentation control, not a security boundary or browser virtual time.
|
|
1114
|
-
|
|
1115
|
-
Unknown partial native failures close the exact Page, preserving healthy peers after confirmed closure; unconfirmed closure fences the session. There is no success receipt, automatic rollback or retry. Scope cleanup never sends a hidden resume: it closes the owned session/connection. Chromium may reset animation state on CDP detachment, so a previous hold acknowledgement does not guarantee remote clocks remain held after connection loss. Hosted-provider equivalence has not been tested.
|
|
1116
|
-
|
|
1117
|
-
## Network policy
|
|
1118
|
-
|
|
1119
|
-
Only explicit trusted-host `Unrestricted` network policy is implemented. `ExactHosts` and `PublicWeb` fail before acquisition. A local debugger endpoint, an input-admission callback or a filtered initial navigation URL does not establish containment for redirects, subresources, popups, workers or DNS. Applications requiring egress restrictions must operate and independently qualify that enforcing boundary. Chromium can select a host-operated proxy at launch; provider routing is configured by its own integration. These choices never silently substitute a browser provider.
|
|
1120
|
-
|
|
1121
|
-
## Testing without a browser
|
|
1122
|
-
|
|
1123
|
-
`effect-browser/testing` opens the real owner over a scripted native engine. `Testing.open(script, options)` returns a `ScriptedSession<E>`: the same `BrowserSession<E>` that Chromium and Browserbase return, and admission, budgets, staleness, dispatch evidence, capture accounting, page holds and typed callbacks are the production code paths. Only the pages and the native outcomes are scripted, so an application, a Toolkit composition or a Layer runs on the pinned Node and Bun with no Chromium process, no Playwright installation and no credentials. The defaults are the ones a Chromium launch would use: an unrestricted policy, a 1280×720 viewport and a ten-second action timeout.
|
|
1124
|
-
|
|
1125
|
-
A script lists documents by address, each with its text and the controls it offers. A control's `id` becomes its issued `elementId`, and observations are numbered `observation-1`, `observation-2`, …, so a scripted model turn or a stored expectation can name a node statically. A `link` or a submit control carries a `destination`, which is both its host-only destination fact and where activating it leads; a plain button navigates through `activates` and reports no destination, as a real `onclick` handler has none, and the schema refuses a destination where a real document would report none. Navigating to an unlisted address reaches an empty document at that address. A checkbox or radio `input` without `checked` starts unchecked, as a real one does. `fillForm` and matched readings take the same steps over the script: a text step records its value in `document.values` and leaves the control, a toggle is clicked only when it differs, options are the observation's issued option IDs, and `next("fill-form", …)` arms the next step, verification read or submit.
|
|
865
|
+
import { Effect, Layer } from "effect";
|
|
866
|
+
import { FetchHttpClient } from "effect/http";
|
|
867
|
+
import { Otlp, OtlpSerialization } from "effect/observability";
|
|
1126
868
|
|
|
1127
|
-
|
|
869
|
+
// Exports to OTEL_EXPORTER_OTLP_ENDPOINT when OTEL_TRACES_EXPORTER=otlp is set.
|
|
870
|
+
const Observability = Otlp.layerFromConfig({ resource: { serviceName: "my-agent" } }).pipe(
|
|
871
|
+
Layer.provide(OtlpSerialization.layerJson),
|
|
872
|
+
Layer.provide(FetchHttpClient.layer),
|
|
873
|
+
);
|
|
1128
874
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
import { BrowserPolicy } from "effect-browser/browser-data";
|
|
1134
|
-
import { Reasons } from "effect-browser/errors";
|
|
1135
|
-
import * as Testing from "effect-browser/testing";
|
|
1136
|
-
|
|
1137
|
-
const shop: Testing.Script = {
|
|
1138
|
-
documents: [
|
|
1139
|
-
{
|
|
1140
|
-
url: "https://shop.test/",
|
|
1141
|
-
text: "We use cookies.",
|
|
1142
|
-
controls: [
|
|
1143
|
-
{
|
|
1144
|
-
id: "accept",
|
|
1145
|
-
kind: "button",
|
|
1146
|
-
label: "Accept all",
|
|
1147
|
-
activates: "https://shop.test/?consent=1",
|
|
1148
|
-
},
|
|
1149
|
-
],
|
|
1150
|
-
},
|
|
1151
|
-
{ url: "https://shop.test/?consent=1", text: "Welcome back." },
|
|
1152
|
-
],
|
|
1153
|
-
};
|
|
1154
|
-
|
|
1155
|
-
it.effect("an unknown click is never replayed", () =>
|
|
1156
|
-
Browser.scoped(
|
|
1157
|
-
Testing.open(shop, { policy: BrowserPolicy.unrestricted({ maxActions: 10 }) }),
|
|
1158
|
-
(browser) =>
|
|
1159
|
-
Effect.gen(function* () {
|
|
1160
|
-
yield* browser.control.next("click", {
|
|
1161
|
-
_tag: "Fail",
|
|
1162
|
-
reason: Reasons.Timeout.make({}),
|
|
1163
|
-
outcome: "unknown",
|
|
1164
|
-
});
|
|
1165
|
-
const observation = yield* browser.initialPage.observe();
|
|
1166
|
-
const accept = { observationId: observation.observationId, elementId: "accept" };
|
|
1167
|
-
|
|
1168
|
-
const first = yield* browser.initialPage.clickElement(accept).pipe(Effect.flip);
|
|
1169
|
-
|
|
1170
|
-
expect(first).toMatchObject({ reason: { _tag: "Timeout" }, outcome: "unknown" });
|
|
1171
|
-
const retry = yield* browser.initialPage.clickElement(accept).pipe(Effect.flip);
|
|
1172
|
-
|
|
1173
|
-
expect(retry).toMatchObject({ reason: { _tag: "Closed" }, outcome: "undispatched" });
|
|
1174
|
-
const clicks = (yield* browser.control.calls).filter((c) => c.operation === "click");
|
|
1175
|
-
|
|
1176
|
-
expect(clicks).toHaveLength(1);
|
|
1177
|
-
expect(clicks[0]).toMatchObject({ dispatched: true, settled: "failed" });
|
|
1178
|
-
}),
|
|
1179
|
-
),
|
|
875
|
+
program.pipe(
|
|
876
|
+
Effect.provide([Chromium.layer(), OpenRouter]),
|
|
877
|
+
Effect.provide(Observability),
|
|
878
|
+
Effect.runPromise,
|
|
1180
879
|
);
|
|
1181
880
|
```
|
|
1182
881
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
`
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
| `capture-data` | Capture options, binary frame and result schemas without session operations |
|
|
1212
|
-
| `page-control` | Explicit host-owned page holds and receipt-based resume |
|
|
1213
|
-
| `chromium` | Self-managed Chromium launch, borrowed loopback attachment and process cleanup |
|
|
1214
|
-
| `browser-runtime` | Supported construction for integrations supplying browser lifetimes |
|
|
1215
|
-
| `testing` | The real owner over a scripted engine: scripts, armed outcomes, a call recorder |
|
|
1216
|
-
|
|
1217
|
-
The root intentionally excludes the Chromium namespace. Import its entry point explicitly.
|
|
1218
|
-
|
|
1219
|
-
## Validation
|
|
1220
|
-
|
|
1221
|
-
Use the pinned workspace and Vite+ commands described in [Contributing](../../CONTRIBUTING.md). Unit tests exercise ownership, callback errors, native-call classification and bounded capture; `test/testing.test.ts` exercises the public scripted engine through the same owner. Native Chromium tests use real loopback pages, capture and page holds, verify that borrowed closure leaves the external browser running, and run the scripted-parity case list against both engines. The `browser` installed consumer has no Browserbase or Effect Agent package; the `agent` consumer adds only the common adapter and framework. Provider-backed native integration remains separately exercised by Browserbase consumers. None of these local checks establishes hosted-provider equivalence.
|
|
882
|
+
The agent's turns, model calls and tool calls are Yielded's spans, with OpenTelemetry's GenAI
|
|
883
|
+
attributes. Every page operation is a
|
|
884
|
+
`Page.<name>` span, such as `Page.click`, `Page.type` or `Page.navigate`, with what its `Action`
|
|
885
|
+
records except text: the target, the subject's role, name and tag, whether input was dispatched,
|
|
886
|
+
`queuedMillis` spent waiting for its turn and the page's replies, and a failure's reason as
|
|
887
|
+
`error.type`. A read that shared another's work says so in `shared`: `joined` or `kept`.
|
|
888
|
+
`Page.prepare`, the policy's preparation, and `Page.guard`, which lasts as long as a hold and holds
|
|
889
|
+
a judge's model call, are its children, as are pointer travel (`Page.move`) and the settle after input (`Page.settle`).
|
|
890
|
+
`Page.snapshot`, `Page.find`, `Page.text`, `Page.title`, `Page.viewport`,
|
|
891
|
+
`Page.screenshot` and `Page.frame` (with
|
|
892
|
+
`source`: a reused screencast `frame` or a new `screenshot`), `Page.zoom`, `Page.ready` and the
|
|
893
|
+
other waits are spans, as are `Plan.replay` and `Plan.locate`, and so is each round trip to the
|
|
894
|
+
page's script (`Page.evaluate`) and its registration (`Page.register`). Each page span reports its cost on the page's own protocol session: `calls`,
|
|
895
|
+
`bytesOut` and `bytesIn` (their parameters and results as JSON) and `waitedMillis`, how long at
|
|
896
|
+
least one call awaited its reply. An operation inside another counts toward both. Calls Playwright
|
|
897
|
+
makes on its own sessions, such as navigation, are not counted. `Chromium.launch`, `Cdp.connect` and
|
|
898
|
+
`Browser.newPage` cover opening a browser; `Browserbase.open` records its session's id and region,
|
|
899
|
+
`Browserbase.release` a release and its outcome, and `Browserbase.holdContext` the wait for a
|
|
900
|
+
stored context another session writes to. `Page.calibrateClock` maps the browser's clock onto the
|
|
901
|
+
host's with three probes, each a `Page.evaluate` of `clock`, and records the fastest answered
|
|
902
|
+
probe's `roundTripMillis`: the round trip to the browser, plus any wait behind the page's own work.
|
|
903
|
+
One that runs alongside a capture shows its failure only there.
|
|
904
|
+
|
|
905
|
+
Page spans never carry typed text, and a script round trip is named by its function alone. A tool
|
|
906
|
+
call's parameters, which hold what a model typed, are Yielded's to record, under its own telemetry
|
|
907
|
+
and redaction. `Page.evaluate` spans are at `Trace` level and the phases
|
|
908
|
+
inside an action at `Debug`: set `Tracer.MinimumTraceLevel` to `"Info"` to keep only the coarser
|
|
909
|
+
spans.
|