effect-browser 0.2.0-beta.9 → 0.3.0-beta.6
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 +707 -1184
- package/dist/Browser-B5AV7zPS.mjs +437 -0
- package/dist/Browser-B5AV7zPS.mjs.map +1 -0
- package/dist/Browser.d.mts +115 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +202 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +295 -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-DVP2IJWm.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-D4281dIi.mjs +618 -0
- package/dist/Page-D4281dIi.mjs.map +1 -0
- package/dist/Page-DYTdVhYa.d.mts +593 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-5MdsRzE5.mjs +314 -0
- package/dist/Plan-5MdsRzE5.mjs.map +1 -0
- package/dist/Plan-CNNOU1Tx.d.mts +112 -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 +54 -0
- package/dist/Stage.mjs +133 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-BtEG93gR.mjs +420 -0
- package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
- package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-DAYjATgC.mjs +137 -0
- package/dist/context-DAYjATgC.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-BkRcLCWZ.mjs +5097 -0
- package/dist/page-BkRcLCWZ.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-CcL0K7S0.mjs +262 -0
- package/dist/url-CcL0K7S0.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +625 -0
- package/src/BrowserError.ts +225 -0
- package/src/BrowserEvent.ts +294 -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 +520 -0
- package/src/Plan.ts +354 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +223 -0
- package/src/Supervisor.ts +353 -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/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +183 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +322 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +159 -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 +302 -0
- package/src/internal/page/lane.ts +301 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +137 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +778 -0
- package/src/internal/pictures/clock.ts +141 -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 +154 -0
- package/src/internal/reading/ready.ts +105 -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 +265 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +93 -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,744 @@
|
|
|
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));
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Here `recordReceipt` is the host's bounded receipt sink. Consumers of the same Layer build share
|
|
77
|
-
selection, budgets, native admission and lifetime; the Layer does not reset an expired session or
|
|
78
|
-
give same-Page operations independent permits. Its acquisition finalizer performs unchecked release and
|
|
79
|
-
stores the receipt. It does not supervise `browser.failure`, and does not turn incomplete cleanup
|
|
80
|
-
into a checked-close error for every consumer. The application must choose that supervision and
|
|
81
|
-
explicit checked closure, or use `Browser.scoped` for a bounded workflow that owns those decisions.
|
|
82
|
-
Calling `Browser.scoped` on the shared live session would close it for every consumer, so it is not
|
|
83
|
-
a per-request wrapper for this pattern.
|
|
84
|
-
|
|
85
|
-
### Receipts outside a race
|
|
86
|
-
|
|
87
|
-
An outer timeout may return `None` while discarding the losing workflow's checked-close error.
|
|
88
|
-
Place the receipt sink outside that race when those facts must remain observable:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
let receipt: ChromiumCleanupResult | undefined;
|
|
92
|
-
const result =
|
|
93
|
-
yield *
|
|
94
|
-
Browser.scoped(Chromium.launch(policy), (browser) => browser.initialPage.navigate({ url })).pipe(
|
|
95
|
-
Effect.timeoutOption("30 seconds"),
|
|
96
|
-
Effect.provide(
|
|
97
|
-
Chromium.layer({
|
|
98
|
-
onCleanup: (value) =>
|
|
99
|
-
Effect.sync(() => {
|
|
100
|
-
receipt = value;
|
|
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
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Use a control whose inspected role and label match your intent. Each frame has its own current
|
|
236
|
-
observation: reading or acting on the scout preserves the stage's exact references. Another
|
|
237
|
-
observation of the same frame retires its predecessor. A reference passed through the wrong
|
|
238
|
-
Page or Frame fails `Stale/undispatched` before input. Mutation invalidation remains conservative
|
|
239
|
-
for the affected page. A detached frame, closed page or old connection generation cannot acquire
|
|
240
|
-
fresh authority. A refusal names the session's own reason first: while the session is closed,
|
|
241
|
-
expired, fenced or paused, work through any issued Page fails `Closed`, `Expired` or `Busy`, not as
|
|
242
|
-
a stale capability. Otherwise a closed Page fails `Closed`, and a Page from an earlier generation,
|
|
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.
|
|
277
|
-
|
|
278
|
-
```ts
|
|
279
|
-
import * as Plan from "effect-browser/plan";
|
|
280
|
-
|
|
281
|
-
// page is an issued Page on the browser's active connection.
|
|
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
|
-
```
|
|
338
|
-
|
|
339
|
-
`events(cursor)` resumes exclusively after that sequence; `events({ at })` includes events
|
|
340
|
-
appended at or after the domain-qualified time. `events()` follows from the current tail when
|
|
341
|
-
the stream executes. Snapshots carry immutable events, global available bounds, eviction facts,
|
|
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`:
|
|
420
|
-
|
|
421
|
-
```ts
|
|
422
|
-
yield * scout.click({ selector: "#advance" }, { admission: { queue: "1 second" } });
|
|
423
|
-
yield * stage.checkpoint({}, { timeoutMillis: 2000, admission: { queue: "500 millis" } });
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
Omitting `admission.queue`, or setting it to zero, refuses conflicting work immediately with
|
|
427
|
-
`Busy/undispatched`. A positive finite Effect duration allows FIFO waiting for that permit.
|
|
428
|
-
The queue defaults to 32 pending callers per Page or registry and 128 across the session.
|
|
429
|
-
`automation.admissionLimits.pendingPerPage` and `pendingPerSession` can set either bound to an
|
|
430
|
-
integer from 1 through 1,024. `Chromium.layer` and `BrowserbaseBrowser.layer` accept those fields
|
|
431
|
-
under their top-level `admissionLimits` option. `QueueFull` reports the refusing scope, maximum and observed count;
|
|
432
|
-
`QueueExpired` means the admission wait expired. Cancellation and either refusal dispatch
|
|
433
|
-
nothing and consume no action allowance. A granted waiter owns the permit before waking, so a
|
|
434
|
-
new arrival cannot overtake it.
|
|
435
|
-
|
|
436
|
-
The operation deadline and queue deadline start when the Effect executes. Waiting counts toward
|
|
437
|
-
`timeoutMillis`, the policy deadline and the browser lifetime; admission never renews them.
|
|
438
|
-
The queue deadline bounds only waiting, so admitted work can finish after that deadline. An
|
|
439
|
-
operation deadline that ends first reports `Timeout`. These options do not queue away a stale
|
|
440
|
-
target, a Page hold, a navigation reservation or a native-wait conflict. The issued Page/frame
|
|
441
|
-
authority remains fixed while waiting; display selection cannot redirect queued input.
|
|
442
|
-
`Capture.start` and `Capture.stream` accept admission through their capture options. Exact-node
|
|
443
|
-
methods keep the host's `ElementAdmission` callback separate from trailing operation options.
|
|
444
|
-
|
|
445
|
-
`page.status` includes passive `admission` state: the active operation, pending count and bound,
|
|
446
|
-
oldest wait age, retained native operations, native wait and stop setup. `session.admission` adds
|
|
447
|
-
aggregate counts and registry state. Native work that dispatched input keeps its Page occupied
|
|
448
|
-
after caller cancellation until the actual work settles or exact positive Page/connection
|
|
449
|
-
retirement proves it unusable. A read, or a mutation cancelled before dispatch, can no longer
|
|
450
|
-
change the page: it stays counted in `nativePending` but leaves the Page usable at once.
|
|
451
|
-
These host snapshots grant no priority and expose no native handles.
|
|
452
|
-
|
|
453
|
-
Navigation stopping and Page closure have reserved, bounded cleanup admission, so a full ordinary
|
|
454
|
-
queue cannot prevent recovery. Page closure uses fail-fast reserved admission and its operation
|
|
455
|
-
deadline when joining an existing close; `admission.queue` does not control those waits. Session closure
|
|
456
|
-
preempts work. Handoff, resume and reconnect refuse
|
|
457
|
-
an existing ordinary permit holder before installing their exclusive lifecycle barrier; handoff
|
|
458
|
-
then drains retained native work that can still change a page within its bound before granting
|
|
459
|
-
operator control.
|
|
460
|
-
|
|
461
|
-
### Exact Pages and Frames
|
|
462
|
-
|
|
463
|
-
The session owns lifecycle, display selection and the page registry. `initialPage` is the Page
|
|
464
|
-
issued on the first connection; it remains that original lifetime fact after reconnect. Browser
|
|
465
|
-
input and reads belong to issued Pages and Frames. Constructing or running an operation on one
|
|
466
|
-
never follows display selection:
|
|
467
|
-
|
|
468
|
-
```ts
|
|
469
|
-
const stage = session.initialPage;
|
|
470
|
-
const scout = yield * session.createPage();
|
|
471
|
-
const navigateStage = stage.navigate({ url: stageUrl });
|
|
472
|
-
|
|
473
|
-
yield * session.selectPage(scout);
|
|
474
|
-
yield * navigateStage; // still navigates the original stage
|
|
475
|
-
const seen = yield * scout.observe({ scope: "viewport" });
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
`session.page(pageInfo)` checks both local and native identity under owner admission.
|
|
479
|
-
`createPage()` returns the Page issued for the new page without selecting it. `selectPage(page)`
|
|
480
|
-
takes a Page this session issued and chooses the displayed page, which is also the page `detach`
|
|
481
|
-
records and `reconnect` resumes on; it never retargets a Page or Frame operation, and a closed or
|
|
482
|
-
stale Page is refused unsent. `Browser.checkPage(session, page)` checks that a Page or Frame
|
|
483
|
-
is one this session issued, by identity, and that its authority is still current; adapters call it
|
|
484
|
-
once when they bind, so a foreign, copied or retired target fails before any work. A child frame
|
|
485
|
-
is reached through `page.frame(frameInfo)`, never by selection. Use `page.describe()`, `page.listFrames()`,
|
|
486
|
-
`page.frame(frameInfo)`, `page.resizeViewport(viewport)` and `page.close()` for exact page work.
|
|
487
|
-
The selected-session actions, `retain`, `pinPage`, `pinFrame` and their target-view types are
|
|
488
|
-
removed. The shared operation contract is `PageOperations`.
|
|
489
|
-
|
|
490
|
-
| Previous API | Current API |
|
|
491
|
-
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
492
|
-
| Session actions, `observe`, `checkpoint`, `ready`, `target` | Issued `Page`/`Frame` methods and immutable `identity` |
|
|
493
|
-
| `retain`, `pinPage`, `pinFrame`, `selectFrame` | `initialPage`, checked `session.page(info)` and `page.frame(info)` |
|
|
494
|
-
| Session `describePage`, `framesOf`, `closePage`, `resizeViewport` | `page.describe`, `page.listFrames`, `page.close`, `page.resizeViewport` |
|
|
495
|
-
| Session capture and PageInfo capture target | `Capture.start(page)` / `Capture.stream(page)` |
|
|
496
|
-
| Session PageControl adapters | `PageControl.state(page)`, `suspend(page)`, `resume(page, receipt)` |
|
|
497
|
-
| `createPage()` returning `PageInfo` | `createPage()` returns the issued `Page`; `page.describe()` reads its metadata |
|
|
498
|
-
| `selectPage(pageInfo)` | `selectPage(page)` with an issued `Page` |
|
|
499
|
-
|
|
500
|
-
Failed metadata acquisition after creation keeps its original dispatch evidence and never
|
|
501
|
-
creates a second page. Page operations preserve typed failures and scoped ownership; a terminal
|
|
502
|
-
Page's retained status and timeline remain readable, while new work refuses undispatched.
|
|
503
|
-
|
|
504
|
-
Each page `createPage` opens is a window of its own, not a tab. Chromium paints only the front
|
|
505
|
-
tab of a window, so a screenshot of a tab behind another waits seconds for a compositor frame, or
|
|
506
|
-
never gets one, and the page that was in front goes behind each new tab. A window of its own is
|
|
507
|
-
painted whether or not another page is in front, so any page can be pictured and read at speed
|
|
508
|
-
and the selected page keeps its capture. With a fixed viewport, each new window's contents are
|
|
509
|
-
set to it, as the first page's are; a provider-managed or preserved viewport is left to the
|
|
510
|
-
browser, which sizes a new window like the last one it showed. A browser that answers the
|
|
511
|
-
request for a window with a protocol error, which means it created nothing, gets tabs from then
|
|
512
|
-
on. `createPage()` serializes creation and adoption under the registry permit and fails immediately
|
|
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.
|
|
521
|
-
|
|
522
|
-
```ts
|
|
523
|
-
const stageInfo = (yield * session.listPages()).find((info) => info.title === "Stage")!;
|
|
524
|
-
const stage = yield * session.page(stageInfo);
|
|
525
|
-
const childInfo = (yield * stage.listFrames()).find((info) => info.parentFrameId !== null)!;
|
|
526
|
-
const child = yield * stage.frame(childInfo);
|
|
527
|
-
|
|
528
|
-
yield * session.selectPage(scout);
|
|
529
|
-
yield * stage.click({ selector: "#advance" });
|
|
530
|
-
const childText = yield * child.readText({ selector: "#status" });
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
Issuance changes no display selection and opens no second connection. Each operation keeps its
|
|
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:
|
|
628
|
-
|
|
629
|
-
```ts
|
|
630
|
-
const read = session.initialPage
|
|
631
|
-
.readText({})
|
|
632
|
-
.pipe(Effect.catchReason("BrowserError", "Busy", () => Effect.succeed({ text: "" })));
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
`Limit` includes measured `dimension`, `maximum` and `observed` fields. For example, an action
|
|
636
|
-
allowance exhausted at two reports `maximum: 2, observed: 2`; it does not count a third call that
|
|
637
|
-
was never admitted. `Configuration` and `Malformed` may name a declared schema-field prefix,
|
|
638
|
-
never a supplied value or unknown property name. `Provider`/`Transport` may carry `status` and
|
|
639
|
-
`RateLimited` may carry `retryAfterMillis`; those fields no longer sit on the outer error. A
|
|
640
|
-
`Provider` failure the native engine raised carries `detail`: the first line of what the engine
|
|
641
|
-
said, such as `elementHandle.click: Target page, context or browser has been closed`, without
|
|
642
|
-
terminal escape codes and at most 512 characters, with every address cut to its origin
|
|
643
|
-
(`https://shop.test/…`) and Playwright's call log dropped, since a path, query or DevTools endpoint
|
|
644
|
-
can carry a credential. It is a host diagnostic, and the agent tools never project it.
|
|
645
|
-
`InitializationError` and the provider's separate resource-error families retain their own
|
|
646
|
-
contracts. The Agent tools project host errors to their compact model-facing vocabulary and
|
|
647
|
-
retain the originals in the bounded, host-only `ToolHost.toolFailures` snapshot.
|
|
648
|
-
|
|
649
|
-
### A navigation you can watch while it loads
|
|
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.
|
|
660
|
-
|
|
661
|
-
```ts
|
|
662
|
-
const operation =
|
|
663
|
-
yield * handle.startNavigation(StartNavigationRequest.make({ url, timeoutMillis: 30_000 }));
|
|
664
|
-
|
|
665
|
-
const early = yield * page.checkpoint({ picture: true }); // what has loaded so far
|
|
666
|
-
const receipt = yield * PageControl.suspend(page); // timers, CSS and parsing stop
|
|
667
|
-
yield * PageControl.resume(page, receipt);
|
|
668
|
-
const { url: loaded } = yield * operation.completed;
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
The owner's permit is released as soon as the navigation is dispatched. While it loads, reads, checkpoints, holds and every other page proceed, and anything that would change _this_ page fails `busy` and `undispatched`. `navigate` runs on the same machinery, so there is one navigator.
|
|
672
|
-
|
|
673
|
-
- `request` is what was asked for, checked before dispatch. `Plan.recordedNavigation` records it once `completed` succeeds.
|
|
674
|
-
- `completed` belongs to that one navigation: a successor reaching the same URL fails it instead of completing it, and its URL is the page that navigated, not whichever page is selected by then. **Interrupting a waiter stops nothing.** The browser keeps loading and nothing is dispatched again.
|
|
675
|
-
- `stop` asks the browser to stop loading. Its acknowledgement is a known outcome: `completed` then fails `interrupted`, the page holds whatever had loaded, and the session stays usable. It does not undo anything the page already did. Concurrent callers share their active stop attempt. A busy refusal or cancellation before dispatch allows a later request only after native setup and its port retire; pending or unconfirmed retirement keeps setup capacity occupied. Once a stop is dispatched, every later caller receives that attempt's recorded result, including failure or interruption, without sending it again. A completed navigation cannot stop its successor.
|
|
676
|
-
- A main-frame loading timeout asks the same stop coordinator to retire that exact navigation. Acknowledged stop completes it with `Timeout/unknown` and leaves the owner open: partial content remains inspectable and deliberate subsequent input is allowed. An explicit stop racing recovery shares its attempt; a completed predecessor cannot stop a successor. The `unknown` outcome still says nothing about page effects before cancellation.
|
|
677
|
-
- A failed or unacknowledged recovery, replacement navigation, detached frame or other unknown native outcome closes that exact Page, with a session fence if closure is unconfirmed. Pinned child-frame timeout also uses Page containment: `Page.stopLoading` acts on the whole page, so automatic frame-local cancellation is not claimed. Explicit stop retains its page-wide meaning.
|
|
678
|
-
- Leaving the operation's scope unsettled retains an unknown outcome and closes its Page; healthy Pages survive confirmed closure. Neither navigation nor a dispatched stop is replayed, and no timeout or acknowledgement promises rollback, an unchanged DOM, or termination of every page timer or worker.
|
|
679
|
-
|
|
680
|
-
A read can inspect a loading Page. If a native read fails because its document was replaced or navigation is in flight, the error is `target-changed` and `undispatched`, which permits another read. A read is never a mutation. A held page is not read at all; take the checkpoint before the hold and keep it.
|
|
681
|
-
|
|
682
|
-
Locally, a hold during an incremental response stops parsing as well as timers: a chunk the server sends meanwhile is not parsed until resume. That is an observation of the pinned Chromium, not a guarantee about hosted sessions.
|
|
683
|
-
|
|
684
|
-
### What is on screen, and what a host may know about it
|
|
685
|
-
|
|
686
|
-
`page.observe()` reads the whole document. `page.observe({ scope: "viewport" })` keeps only text and controls that are on screen and reachable, and says what it left out:
|
|
687
|
-
|
|
688
|
-
```ts
|
|
689
|
-
const seen = yield * page.observe({ scope: "viewport" });
|
|
690
|
-
seen.viewport; // { clippedText, coveredText, uncertainText, unreachableControls, exhausted, ... }
|
|
9
|
+
```sh
|
|
10
|
+
npm install effect-browser@beta effect playwright-core
|
|
11
|
+
npx playwright-core install chromium
|
|
691
12
|
```
|
|
692
13
|
|
|
693
|
-
`
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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.changes({ since, until })` says what visibly changed on a page over a window, in one call to
|
|
126
|
+
the page, element by element, as a `Change`: text that changed, appeared, disappeared or came and
|
|
127
|
+
went (`brief`), a field's value, or the title. Each says what it showed at the window's start and
|
|
128
|
+
end, how often that changed, the lowest and highest of a number that changed more than once, when
|
|
129
|
+
it last changed before the window, so news stands apart from what keeps changing, and its
|
|
130
|
+
`subject` with the context that binds it: a price under its row and column. A field's value reads
|
|
131
|
+
`••••` unless `unmask` is set, and a secret field's always does. A change names its `cause`, the
|
|
132
|
+
trusted input it followed, only where it was that input's doing. It must not have been in flux: in
|
|
133
|
+
the 10 s before the input it had not changed, and nothing had come into or left what it sits in, so
|
|
134
|
+
a ticker's next tick or a feed's next line names no click. Then it changed within 3 s inside what
|
|
135
|
+
the input acted on or its row, form, dialog or controlled element, so "Order placed" a server's
|
|
136
|
+
reply later keeps its click; or elsewhere within 500 ms, on a page where nothing had changed in the
|
|
137
|
+
2 s before the input, so a menu a portal puts at the end of the page keeps its click, and a chat
|
|
138
|
+
line beside a ticking board does not. What holds the input's target, as a menu holds its item, does
|
|
139
|
+
not count against it. Against inert clicks on boards whose 2 to 20 cells tick every 0.25 to 6 s,
|
|
140
|
+
the rule credited none of 401 ticks and none of 96 chat lines, where a second's quiet on the element
|
|
141
|
+
and 500 ms anywhere had credited about half the ticks of the slower boards and every chat line.
|
|
142
|
+
What a busy page costs is the credit for an effect out of a click's reach, which a moment then
|
|
143
|
+
tells as a step; on a still page, an unrelated line 250 ms after a click is still credited.
|
|
144
|
+
|
|
145
|
+
A page records once something reads its changes: the first read starts it, and finds nothing yet.
|
|
146
|
+
From then the page's own session starts the recorder at the start of each later document, once it
|
|
147
|
+
is parsed, until nobody has read it for two minutes; a page nobody reads records nothing and costs
|
|
148
|
+
nothing. The first read also registers the recorder, in the same round trip, and every later read
|
|
149
|
+
is one call. A browser whose clock no capture has mapped yet maps it on its first read of changes,
|
|
150
|
+
once, in four round trips more. While it runs, a MutationObserver marks what changed: a text-only
|
|
151
|
+
element, as most prices are, is read at once, and anything else, with whether it was in view, as
|
|
152
|
+
the page renders it, through an IntersectionObserver that forces no layout. A change counts as seen
|
|
153
|
+
when it was in the viewport as the page rendered it, its style showing it, so one scrolled away
|
|
154
|
+
later is still told, and something removed or hidden only if it was in view before. On a
|
|
155
|
+
2,000-cell table rewritten every 50 ms, locally, the page's busy time went from about 100 ms per
|
|
156
|
+
3 s to about 230 recording, where an empty observer costs about 180.
|
|
157
|
+
|
|
158
|
+
The record keeps 256 elements with their last 32 changes, for a minute. On a busier page, an
|
|
159
|
+
element that keeps changing gives way first, then one never seen in view, then one that has gone,
|
|
160
|
+
so news stays; whatever gives way, or finds no room, is counted in `dropped`, and `from` moves
|
|
161
|
+
past it: the record never claims to be whole where it is not. A window can start at a previous
|
|
162
|
+
read's `Changes`, continuing exactly where it ended on the page's own clock, or at a frame's
|
|
163
|
+
paint, and end at a frame's paint, so a narrator airing a frame late is told nothing that frame
|
|
164
|
+
does not show. Times are host milliseconds through the browser's one clock mapping, as frames'
|
|
165
|
+
are. The record does not see pictures, a canvas, frames, shadow roots or SVG, a value a script
|
|
166
|
+
sets, or a class that reveals an element it has not seen before, such as a toast already on the
|
|
167
|
+
page, though its later changes it does.
|
|
168
|
+
|
|
169
|
+
A page's timeline has three tracks on the browser's host clock: its events, what changed on it,
|
|
170
|
+
and its frames. `page.window({ since, until })` reads them over a window, a `Moment.Window`: the
|
|
171
|
+
events and frames the page keeps, at no call, and its changes, in one call, the first starting its
|
|
172
|
+
record as any read of changes does. `since` is a previous window, to go on exactly where it ended, a
|
|
173
|
+
frame, a host time, or a `Duration` back from `until`; `until` is a frame or a host time, now by
|
|
174
|
+
default. A consumer that airs a frame seconds after it was painted reads the window that ends at
|
|
175
|
+
that frame, so that a line it writes now tells what its viewers will see. A part that cannot be
|
|
176
|
+
read is in `missing`, with why: a window fails only for bounds that are not finite, or a negative
|
|
177
|
+
reach back.
|
|
178
|
+
`Moment.account(window)` says what was so at the window's end. Its `changing` are the changes still
|
|
179
|
+
under way: what kept changing, more than once in the window or already before it, and changed
|
|
180
|
+
again within twice its own pace before the end, as a ticking price does. Its `settled` are the
|
|
181
|
+
rest, each showing what it settled at, as a price that ticked and stopped does. Its `stillFor` is
|
|
182
|
+
how long the page had been still, by the last change in view its record shows or the last paint
|
|
183
|
+
among its screencast frames; a screenshot shows the page, not when it changed. Frames show paint
|
|
184
|
+
only while a capture runs, and the record does not see a canvas, so a canvas that draws with no
|
|
185
|
+
capture running reads as still. A wait for a still screen is `Page.ready({ quietMillis })`, which
|
|
186
|
+
also holds through a stalled connection.
|
|
187
|
+
|
|
188
|
+
`Moment.capture` is a window that ends at a picture of the page now. It takes the picture first,
|
|
189
|
+
then the window up to its paint, from where a previous window or moment ended, so the changes hold
|
|
190
|
+
nothing the picture does not show. A picture, a read of changes or an outline that cannot be had is
|
|
191
|
+
missing from the moment, with why, and the moment is still made. `Moment.toPrompt` leads with what
|
|
192
|
+
changed: news and what settled first, then what was still changing, by the moment's account, with
|
|
193
|
+
the cells of a column that changed together on one line; a change that kept changing says whether
|
|
194
|
+
it still was at the moment or since when it had settled, so a narrator need not assert a value that
|
|
195
|
+
is still moving. It names an action as what a change followed, or as a step: where changes followed
|
|
196
|
+
it but none names it, as when its effect landed out of its reach on a busy page; where its effect is
|
|
197
|
+
drawn, such as a click on a canvas, which only the screenshots show; or where it came before the
|
|
198
|
+
record began. Hovers, scrolls and attempts that nothing followed are left out, and a failed action is told
|
|
199
|
+
only as failed: its error is advice to the caller that acted, and may name a ref. A moment whose
|
|
200
|
+
record saw none of its window, as a page's first, whose read starts the record, says that what
|
|
201
|
+
changed was not recorded and lists every step instead; one whose record began within the window
|
|
202
|
+
says nothing changed only after that. What it could not read, it names, with why, and a last frame
|
|
203
|
+
that is not the picture is not shown as the moment.
|
|
204
|
+
|
|
205
|
+
`Plan` rehearses a walk once and replays it later, near live, with no model call.
|
|
206
|
+
`Plan.fromEvents(page.recentEvents)` keeps one page's completed actions with their subjects and
|
|
207
|
+
the options they were given, its navigations with the address asked for and the one reached, and
|
|
208
|
+
what was typed as an input slot named for its field, never the text itself. A navigation to an
|
|
209
|
+
address with a credential withheld is an input slot too, `address`, so replay goes where the caller
|
|
210
|
+
says, never to the address without its credential. `Plan.replay(page,
|
|
211
|
+
plan, { inputs })` takes the steps in turn: before each that acts, it waits for `Page.ready`
|
|
212
|
+
(`settle` sets how, or `false` not to wait), finds the one element the step's subject names with
|
|
213
|
+
`Plan.locate`, and acts on it. An element that does not repeat every part of the recorded context
|
|
214
|
+
is another subject: its row read whole, so a row named "Wrapped BTC" is not the row "BTC" (laid out
|
|
215
|
+
again without rows, as cards, one of its fields must start with the row's words), its column where
|
|
216
|
+
it is under one, and its label and heading read the same or found as words in order. So the one
|
|
217
|
+
element left once the recorded one has gone, such as Alice's "Remove" after Bob's, drifts rather
|
|
218
|
+
than stands in. The rest rank by how much they read the same; a tie is `Ambiguous`, since there is
|
|
219
|
+
no ordinal. Text typed into whatever had focus went into the focused field, which the action
|
|
220
|
+
records as its subject, so replay inserts into that field wherever focus has gone, retaining its
|
|
221
|
+
existing text. Caret positions are not recorded: insertion uses the field's caret when focused.
|
|
222
|
+
A press recorded on a subject resolves and focuses it before sending keys. Text first
|
|
223
|
+
typed into a secret field is typed only into one. An element step never falls back to coordinates.
|
|
224
|
+
A step recorded at a point presses the same place within the element found, brought into view
|
|
225
|
+
first, and only when a press there reaches that element, not something over it. Replay stops at
|
|
226
|
+
the first step it cannot take, with a `ReplayError` naming the step and why: `Missing`,
|
|
227
|
+
`Ambiguous`, `Drifted` (nothing with the subject's role and name repeats its context, or the walk
|
|
228
|
+
ended on another site), or the step's `BrowserError`. Nothing is replayed automatically, and a plan
|
|
229
|
+
of another version does not decode.
|
|
230
|
+
|
|
231
|
+
`Browser.Options.guard` is the input policy. Its `InputRequest` schema contains the action, the
|
|
232
|
+
resolved element, the page's URL and title, and the `facts` the page's structure establishes:
|
|
233
|
+
`form-submit`, `cross-origin`, `download`, `upload`, `secret` (typing with `type` into a
|
|
234
|
+
password, one-time code or card field, or submitting a form that holds a filled one), `scripted` (the activated
|
|
235
|
+
control has no effect of the browser's own, so only page script decides what happens) and
|
|
236
|
+
`opaque` (nothing names what receives the input, such as a canvas or an unlabelled icon). More
|
|
237
|
+
than one may apply. They describe what the input activates, found as the browser finds it: the
|
|
238
|
+
submit button around a painted label, an SVG link around a shape, an image-map area. Hovering and
|
|
239
|
+
scrolling activate nothing and carry none. `point` is present for literal pixel targets; a ref's
|
|
240
|
+
coordinates are resolved after approval so preparation never scrolls.
|
|
241
|
+
|
|
242
|
+
Facts never come from what an element's text says: words change meaning with context and
|
|
243
|
+
language, so a payment, a deletion or a consent is for a judge to recognise. The request carries
|
|
244
|
+
the evidence one needs: the target's name and description, its dialog, the heading before it, the
|
|
245
|
+
text beside it in its row or form, and the form's fields with their types and autocomplete tokens.
|
|
246
|
+
Fields carry whether they are filled, never their values. All of it is page text the page
|
|
247
|
+
controls, to read as evidence and never as instructions. Text typed into a `secret` field reaches
|
|
248
|
+
the guard and the recorded `Action` as `redacted`, and its key events as `Unidentified` keys;
|
|
249
|
+
`press` sends and records the keys it is given, so type secrets with `type`.
|
|
250
|
+
It covers clicks, drags, typing, keys, selection, hover, scroll and navigation, including a new
|
|
251
|
+
tab's destination.
|
|
252
|
+
|
|
253
|
+
The policy's effect succeeds to allow, fails with `PolicyDenied` to refuse, or waits for a signal
|
|
254
|
+
the consumer owns to hold. For example:
|
|
698
255
|
|
|
699
256
|
```ts
|
|
700
|
-
|
|
701
|
-
|
|
257
|
+
import { Effect } from "effect";
|
|
258
|
+
import { PolicyDenied } from "effect-browser/BrowserError";
|
|
259
|
+
import * as Chromium from "effect-browser/Chromium";
|
|
260
|
+
|
|
261
|
+
const browser = Chromium.layer({
|
|
262
|
+
guard: (request) =>
|
|
263
|
+
request.facts.includes("secret") && request.facts.includes("cross-origin")
|
|
264
|
+
? Effect.fail(new PolicyDenied({ detail: "Secrets are never sent to another site." }))
|
|
265
|
+
: Effect.void,
|
|
266
|
+
});
|
|
702
267
|
```
|
|
703
268
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
is emitted only for applicable native inputs or reviewed ARIA roles. State that can change an
|
|
713
|
-
input decision participates in the exact-node fresh-facts check; field values and destinations
|
|
714
|
-
remain excluded from the model projection.
|
|
269
|
+
`effect-browser/Policy` builds guards for a browser with nobody watching. A judge reads what an
|
|
270
|
+
input means. `Policy.reviewer()` asks the `LanguageModel` in context for a structured review, and
|
|
271
|
+
`Policy.decider` asks a `DecisionModel`, such as Jev through `@effect/ai-typesafe`. Both give a
|
|
272
|
+
`Judgement`: for each `Risk` (`financial`, `account`, `access`, `deletion`, `communication` and
|
|
273
|
+
`secret`), the probability that the input does it, and the probability that the user's task asks
|
|
274
|
+
for it. `Policy.escalate(first, second)` asks `second` only when `first` is unsure.
|
|
275
|
+
`Policy.make({ judge })` denies an input with a likely risk the task does not ask for, saying which
|
|
276
|
+
and why. `origins` keeps input to some origins, and `deny` refuses facts outright:
|
|
715
277
|
|
|
716
278
|
```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.
|
|
279
|
+
import { Effect, Layer } from "effect";
|
|
280
|
+
import * as Chromium from "effect-browser/Chromium";
|
|
281
|
+
import * as Policy from "effect-browser/Policy";
|
|
725
282
|
|
|
726
|
-
|
|
283
|
+
const browser = Layer.unwrap(
|
|
284
|
+
Effect.gen(function* () {
|
|
285
|
+
const judge = yield* Policy.reviewer();
|
|
727
286
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
admit: (facts) => facts.inputType !== "password" && facts.autocomplete !== "cc-number",
|
|
732
|
-
});
|
|
287
|
+
return Chromium.layer({ guard: Policy.make({ judge, origins: ["https://shop.example"] }) });
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
733
290
|
```
|
|
734
291
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
are
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
292
|
+
The task comes from `Policy.Task`, which `effect-agent-browser`'s tools provide to their inputs
|
|
293
|
+
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
|
|
294
|
+
text and the facts as trusted, and the page's text only as evidence. It never sees the agent's own
|
|
295
|
+
words. A judgement only adds to structure: a `secret` fact counts whatever the judge reads. A judge
|
|
296
|
+
that fails or exceeds `timeout` (30 seconds) leaves the input unjudged. `make` then denies the input
|
|
297
|
+
if it has any fact, with the judge's failure as the `PolicyDenied` cause, and lets an input without
|
|
298
|
+
facts, such as a same-origin link, go ahead. Through `@effect/ai-openrouter` 4.0.0, structured output needs
|
|
299
|
+
`strictJsonSchema: true` in the model's config, or OpenRouter drops the response format and every
|
|
300
|
+
review fails to decode.
|
|
301
|
+
|
|
302
|
+
Holds use `policyTimeout`, a finite positive duration defaulting to five minutes, separately from
|
|
303
|
+
the action timeout. They do not keep the page locked. After approval, the library verifies the same
|
|
304
|
+
document, target and relevant facts before sending input: a control's name is bound but other page
|
|
305
|
+
text, such as a live price, is not; the URL is bound without a fragment that only marks a place on
|
|
306
|
+
the page, as scroll-spy and feed pages rewrite while scrolling (a `#/` or `#!` hash route stays
|
|
307
|
+
bound). Changed targets fail undispatched; the library never retries the action or the policy
|
|
308
|
+
automatically. A pointer press is checked again once the pointer has arrived and the page has had a
|
|
309
|
+
frame to react: the approved control must still receive the press point, so a control that appears
|
|
310
|
+
under the pointer, such as a hover menu, stops the action before the button goes down. A link,
|
|
311
|
+
button or other control nested inside the target between it and the press point stops it too, since
|
|
312
|
+
the approval inspected the target, not that control. Each
|
|
313
|
+
further press of a double or triple click is checked the same way after the earlier clicks' handlers
|
|
314
|
+
have run. Typing approves its field once. Plain text then goes into it in one insertion, which no
|
|
315
|
+
key handler can split, so the page sees one `input` event and no keys, and a paste's time does not
|
|
316
|
+
grow with its length: 2,000 characters took half a second at a 70 ms round trip. Typing key by key,
|
|
317
|
+
as a presenter's view does, checks that the approved control still has focus once the keys before
|
|
318
|
+
are answered, before each space, which could press a focused button, and after its last key, so a
|
|
319
|
+
key handler that moves focus stops the typing before its next space; each check is about two
|
|
320
|
+
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
|
|
321
|
+
receipts. Without a guard, actions are allowed and nothing is revalidated. Canvas and opaque frames
|
|
322
|
+
expose their outer element's metadata.
|
|
323
|
+
|
|
324
|
+
A presenter's view reaches an off-screen ref target with visible wheel input before the pointer
|
|
325
|
+
moves to it. Scroll attempts are bounded and may use one instant fallback. A denied or held action
|
|
326
|
+
does not scroll. After scrolling, a guarded action checks the original target again; a page handler
|
|
327
|
+
that changes its meaning can therefore stop it after its wheel input but before a click, and the
|
|
328
|
+
failure is undispatched: travel toward a press is not the action's input. Drag endpoints are
|
|
329
|
+
resolved together in the final viewport, and checked under the pointer, before the button is
|
|
330
|
+
pressed.
|
|
331
|
+
|
|
332
|
+
A view's pointer follows the planner its presenter was given, `Motion.lognormal` unless another.
|
|
333
|
+
That tuned two-stroke sigma-lognormal model is evaluated every 16.7 ms, but a move goes only when
|
|
334
|
+
the pointer reaches a new pixel; the exact destination lands at the model's end time, so only that
|
|
335
|
+
final move can repeat the position before it. A custom `plan(from, to)` returns a complete
|
|
336
|
+
schedule with finite coordinates and nondecreasing absolute `afterMillis` offsets, at most 2,048
|
|
337
|
+
samples and 5,000 milliseconds, ending at the exact destination. The browser decodes it with
|
|
338
|
+
`Motion.Plan` into its own copy, then checks the endpoint; invalid plans fail with
|
|
339
|
+
`InvalidRequest` before their track or input is sent. Equal-time samples are retained. Plain input
|
|
340
|
+
never plans a glide: its pointer jumps, or moves in eight short steps while dragging.
|
|
341
|
+
|
|
342
|
+
For recorded human strokes, install the optional `effect-browser-human-strokes` package and give
|
|
343
|
+
its planner to the presenter:
|
|
769
344
|
|
|
770
345
|
```ts
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
page.fillForm({
|
|
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.
|
|
346
|
+
import * as HumanStrokes from "effect-browser-human-strokes";
|
|
347
|
+
import * as Presentation from "effect-browser/Presentation";
|
|
810
348
|
|
|
811
|
-
|
|
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 });
|
|
349
|
+
const presenter = yield * Presentation.make({ motion: yield * HumanStrokes.motion });
|
|
830
350
|
```
|
|
831
351
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
352
|
+
That package bundles 32,130 attributed CC BY 4.0 strokes, preserving their original sample times.
|
|
353
|
+
The core package includes no stroke data. Every glide reserves its full bounded schedule before
|
|
354
|
+
its published clock starts, so delayed replies cannot stretch a dense stroke through backpressure.
|
|
355
|
+
At most 2,112 input commands and reservations are owned at once; ordinary input retains its
|
|
356
|
+
64-command admission limit. Actions still await their replies before succeeding, and interruption
|
|
357
|
+
stops the unsent suffix and releases held input.
|
|
358
|
+
|
|
359
|
+
Typing sends key pairs for printable US characters, plainly and in a view; other text uses
|
|
360
|
+
Unicode insertion. A typed space or letter can press a focused button, toggle a box, follow a
|
|
361
|
+
link or change a select, so `type` refuses before any input when its `into` ref is not a text field
|
|
362
|
+
or, without `into`, when focus is on such a control; `press` sends keys to those. With `secret`,
|
|
363
|
+
`type` also refuses a field the page does not mark secret, so a password goes only where the page
|
|
364
|
+
hides it, as a replayed password does. A view types at its pacing's words a minute, 70 by
|
|
365
|
+
default, including slower word starts, with key holds around 110 ms that can overlap. The ordered schedule releases a repeated
|
|
366
|
+
physical key before pressing it again. Keys follow that schedule without waiting for each network
|
|
367
|
+
reply. Pending replies are bounded and drained before an action succeeds; interruption stops new
|
|
368
|
+
input and releases every submitted held key. Shortcut chords retain Playwright’s platform-specific
|
|
369
|
+
editing behavior. Before each key, typing and repeated presses check that the page is still in the
|
|
370
|
+
document the action began in, and stop with a dispatched `NotActionable` once it has moved on. The
|
|
371
|
+
browser reports a new document as it commits, so a key sent within about one protocol round trip of
|
|
372
|
+
that commit can still reach it. Under a guard, typing's submit Enter and each repeated Enter or
|
|
373
|
+
Space are first checked against the approved element.
|
|
374
|
+
|
|
375
|
+
After a click, a key, a submit or a scroll, the page settles before the action returns: one call
|
|
376
|
+
waits a task and a frame in the page, and Chromium answers it only once a navigation the input
|
|
377
|
+
asked for, by a link, a form or a handler's timer, has committed. A document that committed is then
|
|
378
|
+
waited for until it is parsed, within 5 seconds. `pushState` and a 204 answer wait for nothing. A
|
|
379
|
+
navigation a handler starts once a fetch answers comes too late for any wait; the next look sees
|
|
380
|
+
it, and a picture never shows a document the page has left. A tab the input opens is discovered and
|
|
381
|
+
registered before the action returns, within its action timeout. An unresolved opening fails with
|
|
382
|
+
dispatched input, and closing the page or losing its browser wakes the wait with `Closed`.
|
|
383
|
+
|
|
384
|
+
## Presenting pages
|
|
385
|
+
|
|
386
|
+
`Presentation` performs input for viewers of a page, and `Stage` decides which page they see.
|
|
847
387
|
|
|
848
388
|
```ts
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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.
|
|
389
|
+
import { Effect, Stream } from "effect";
|
|
390
|
+
import { Browser } from "effect-browser/Browser";
|
|
391
|
+
import type { Frame } from "effect-browser/Frame";
|
|
392
|
+
import * as Presentation from "effect-browser/Presentation";
|
|
393
|
+
import * as Stage from "effect-browser/Stage";
|
|
870
394
|
|
|
871
|
-
|
|
395
|
+
declare const encode: (frame: Frame) => Effect.Effect<void>; // the application's own output
|
|
872
396
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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" }));
|
|
397
|
+
const program = Effect.gen(function* () {
|
|
398
|
+
const stage = yield* Stage.make({ quality: 80 });
|
|
399
|
+
const presenter = yield* Presentation.make();
|
|
400
|
+
const page = yield* (yield* Browser).newPage("https://example.com");
|
|
401
|
+
const shown = yield* stage.present(page); // Presented { page, at, latency }
|
|
402
|
+
|
|
403
|
+
yield* stage.frames.pipe(Stream.runForEach(encode), Effect.forkScoped);
|
|
404
|
+
yield* presenter.view(page).click("e12"); // glides, then clicks, as a person would
|
|
405
|
+
});
|
|
887
406
|
```
|
|
888
407
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
408
|
+
A presenter owns one drawn pointer. `presenter.view(page)` is the page with its actions performed:
|
|
409
|
+
each waits first as a person reacts, by `Presentation.human`'s medians, 280 ms after an expected
|
|
410
|
+
change, 600 ms after a new document and 1 second on another page; the pointer glides from where
|
|
411
|
+
viewers last saw it, on whichever page; a field is clicked before typing; and the wheel turns in
|
|
412
|
+
100 px notches, in bursts of up to nine. One view acts at a time. The time a view spends showing an
|
|
413
|
+
action, its glides, holds and typing, has a budget of its own outside `actionTimeout`. `page`
|
|
414
|
+
itself stays plain, and plain input on any page never waits for a view. `view.aim(target)` starts
|
|
415
|
+
the glide as soon as a target is known, as when it appears in a model's streamed tool call; the
|
|
416
|
+
view's next action completes it if it acts on that target, and stops it where it is otherwise. An
|
|
417
|
+
aim records no action, and does nothing under an input guard.
|
|
418
|
+
|
|
419
|
+
A stage is one per live output. `present(page, { at })` switches it at `at`, host monotonic
|
|
420
|
+
milliseconds on the frame clock, or at once: it starts the new page's capture ahead, waits for its
|
|
421
|
+
first frame, then for input already under way on the old page, and turns, stopping the old capture
|
|
422
|
+
after. The captures overlap, within a session as across two: on Browserbase, two captures in one
|
|
423
|
+
session ran together at the frame rate of one, where stopping first left 250 to 295 ms dark.
|
|
424
|
+
`Presented.at` is when the switch took effect, no earlier than the frames it followed or the new
|
|
425
|
+
page's first, and `latency` how long after `at`. A first frame that does not come in time fails
|
|
426
|
+
`present` with `Timeout`, and the old page stays. A capture that fails restarts on its page while the
|
|
427
|
+
page and its browser stand. A still page sends no frames until it changes, so a switch's first frame
|
|
428
|
+
can be older than the frames before it. A delay line, a liveness rule, redaction and repeating a
|
|
429
|
+
held frame stay the application's.
|
|
430
|
+
|
|
431
|
+
`Browser.now`, event stamps, frame `receivedAt` and a window's bounds share host monotonic milliseconds
|
|
432
|
+
from the clock captured when the browser is made. They remain ordered across wall-clock corrections.
|
|
433
|
+
Page operations also pace input and measure their deadlines on that clock, so a caller running
|
|
434
|
+
under another `Clock`, such as a `TestClock`, cannot stall an action or a page's turns.
|
|
435
|
+
Compare these stamps only within that clock: they are not epoch dates or comparable across hosts.
|
|
436
|
+
`Frame.timing` distinguishes `BrowserPaint` from `Screenshot`. Native frames retain browser epoch
|
|
437
|
+
milliseconds in `timestamp` and map them to `hostTime`, with an explicit clock uncertainty.
|
|
438
|
+
Screenshot fallbacks have only a host capture interval; their `timestamp` getter is undefined.
|
|
439
|
+
Windows and a moment's frame captions use `hostTime`, so delayed delivery cannot make old paint current.
|
|
440
|
+
A picture states how old it may be. `Page.frame({ maxAge, after })` serves the newest screencast
|
|
441
|
+
frame when it has the viewport's size and was painted at most `maxAge` ago, at the earliest its
|
|
442
|
+
timing allows: 250 ms by default, while 0 always takes a new screenshot. With `after: "input"` the
|
|
443
|
+
frame must also follow the page's latest submitted input, including input of an interrupted action,
|
|
444
|
+
and the read waits for the action in flight: what a caller that has just acted needs.
|
|
445
|
+
`Page.screenshot` always applies that rule, so a caller that acts and then looks sees what its
|
|
446
|
+
action did; a deck or a narrator that wants a picture of a stated age reads `frame`, which a frame
|
|
447
|
+
that qualifies serves at once, even while an action runs. A screencast sends only changes and can
|
|
448
|
+
miss a final paint, so a frame is only ever as current as its age. No read reuses a frame painted
|
|
449
|
+
before the page's current document began, when the page's own session saw its main frame commit
|
|
450
|
+
it: a page that keeps painting while the next document loads would otherwise leave its own frames
|
|
451
|
+
the newest when the navigation returns. Otherwise a new screenshot is
|
|
452
|
+
taken, which `frame` returns as a `Screenshot`-timed frame. A screenshot, which the agent's
|
|
453
|
+
observations take, and a `Moment`'s picture use `after: "input"`, so a stopped capture, a lost
|
|
454
|
+
final paint or later input never presents older paint as the page an action left.
|
|
455
|
+
|
|
456
|
+
A new picture goes on the page's own protocol session. Where a device pixel is a CSS pixel and
|
|
457
|
+
nothing is cropped, it is one `Page.captureScreenshot`; a crop, or another device pixel ratio, adds
|
|
458
|
+
the page's layout metrics for Playwright's clip formula. Where Playwright knows no viewport, as in
|
|
459
|
+
the default context over CDP, the page's first picture also learns it. Chromium takes a clipped
|
|
460
|
+
picture through the device emulation of the session that asks for it, then restores the view's size
|
|
461
|
+
and what that session emulated, so a page's pictures go one at a time. Where Playwright emulates the
|
|
462
|
+
viewport, as `Chromium.layer` does, a crop or a scaled picture is Playwright's own screenshot, on its
|
|
463
|
+
own session, which sends Playwright's 335 KB injected script with a document's first one, as it
|
|
464
|
+
would over CDP once a caller gives Playwright a viewport. Elsewhere, as on Browserbase, whose own
|
|
465
|
+
session emulates the screen, the page's own session holds a copy of the screen the page reads from
|
|
466
|
+
the page's first clipped picture on, at two calls once, so a crop leaves the screen as it was, and
|
|
467
|
+
`device-width` media queries with it. The copy is the screen as the page read it then, and every
|
|
468
|
+
later picture restores it, so a session that changes the screen afterwards sees its change undone.
|
|
469
|
+
When the library's session leaves the page, as when a kept session's process ends, Chromium clears
|
|
470
|
+
the emulation, as a crop used to.
|
|
471
|
+
|
|
472
|
+
Reads go to the page script in an isolated world. The page's own session registers the script at the
|
|
473
|
+
library's first read of the page, so every later document runs it from its start: a document's
|
|
474
|
+
first read takes two round trips and later reads one. Tabs the library only tracks are left alone,
|
|
475
|
+
and the clock probe runs in a world of its own.
|
|
476
|
+
Every operation is recorded as an `Action`, also when its caller interrupts it. An element or point
|
|
477
|
+
action records its `subject`, and a drag where it ended (`to`): the role, accessible name, tag and
|
|
478
|
+
context of what it found, read when its target was resolved, before the pointer moved or any input
|
|
479
|
+
was sent, so a click that removes its element still names it: the same subject `Page.find` gives. A ref is
|
|
480
|
+
reused by later outlines, so read `subject` rather than resolving `target` against a later snapshot.
|
|
481
|
+
|
|
482
|
+
Every renderer reads the same host wall clock, so the browser keeps one clock mapping for all of its
|
|
483
|
+
pages. Opening a browser measures nothing: the browser's first capture measures the mapping, with
|
|
484
|
+
three read-only probes in a world of their own, and waits for that one estimate. A busy page answers
|
|
485
|
+
each probe late, behind its own work, so the probes it answered within two seconds serve: the
|
|
486
|
+
estimate is as uncertain as the page made the wait, and the capture fails, undispatched, only if
|
|
487
|
+
the page answered none. A later capture starts with the estimate there is. While frames flow, a
|
|
488
|
+
capture measures again once the estimate is ten seconds old, and every frame is timed by the
|
|
489
|
+
browser's newest estimate, so a wide first estimate narrows once a probe finds the page idle, and
|
|
490
|
+
hours on air leave frames as certain as a fresh capture's. An estimate says less about the offset
|
|
491
|
+
as it ages, by up to 100 parts per million of its age, and a frame carries its estimate's
|
|
492
|
+
uncertainty at its paint. A new measurement
|
|
493
|
+
replaces the estimate only if it says more about the offset than the aged one, or if its interval
|
|
494
|
+
cannot contain the current offset (the clocks moved); a probe slowed by one busy tab cannot skew
|
|
495
|
+
every tab's stamps. Transport asymmetry remains in the reported uncertainty.
|
|
496
|
+
|
|
497
|
+
Once a capture has mapped the clock, mouse input and raw text-key events carry the epoch time each
|
|
498
|
+
was meant for; until then Chromium stamps them as it receives them, and input never waits for a
|
|
499
|
+
measurement. Shortcut chords retain Playwright's platform behavior, and Unicode insertion has no
|
|
500
|
+
timestamp field.
|
|
501
|
+
|
|
502
|
+
`Page.captureStats()` counts, over the page's life or with `{ window }` over up to the latest
|
|
503
|
+
minute: received and accepted frames, missing timestamps, `late` frames (Chromium encodes several at
|
|
504
|
+
once and finished these after a newer one; they are dropped, never reordered, and a busy machine
|
|
505
|
+
makes more of them), frames dropped for their size (`foreignSize`) or for coming during one of the
|
|
506
|
+
library's own clipped pictures (`duringPictures`), frames a reader `lost` by falling behind, and
|
|
507
|
+
paint-time gap totals/minimum/maximum/last. `ackBacklog` is the acknowledgements still unanswered:
|
|
508
|
+
Chromium sends a frame only while few are, so a backlog over a slow connection means fewer frames.
|
|
509
|
+
Chromium draws a clipped or scaled screenshot of the page, taken
|
|
510
|
+
from any session, into its running screencast. Readers never receive a frame whose picture has
|
|
511
|
+
another shape than its device. Frames of another device size wait, in order: the page's own size
|
|
512
|
+
returning drops them, while the page reporting a viewport of their size, or their lasting a second,
|
|
513
|
+
delivers them as a real resize, so a page that cannot answer, or that measures in other units as
|
|
514
|
+
under browser zoom, is never stalled. `foreignSize` therefore also counts some of the page's own
|
|
515
|
+
frames: those of a new size still unconfirmed when another size replaced it, when the capture
|
|
516
|
+
stopped, or beyond the 16 held. The library's own clipped pictures are left out while they are
|
|
517
|
+
taken: from the picture's call until 50 ms after its reply, by when a frame arrived or by its paint
|
|
518
|
+
time widened by the clock's uncertainty either side. A crop on the page's own session, as over CDP
|
|
519
|
+
and on Browserbase, is drawn at the crop's own size, so there only frames of another size are left
|
|
520
|
+
out, and the page's own keep reaching readers. Where Playwright emulates the viewport, as
|
|
521
|
+
`Chromium.layer` does, a crop keeps the device's size, so every frame in that window is left out,
|
|
522
|
+
the page's own included: the capture pauses for the picture's round trip, plus 50 ms, plus twice
|
|
523
|
+
the clock's uncertainty, about 100 ms a zoom locally, and a crop with the viewport's own
|
|
524
|
+
proportions never reaches readers either. A clip someone else takes passes where Chromium keeps
|
|
525
|
+
the device's size during it and the clip has the viewport's proportions, and on a page that paints
|
|
526
|
+
nothing after it, a clip of any shape lasts long enough to pass for a resize.
|
|
527
|
+
Each page's own session holds focus emulation, so a tab behind another keeps painting and its
|
|
528
|
+
capture keeps delivering, whatever else is attached; a capture starts once the browser has
|
|
529
|
+
confirmed it, and fails with `Timeout` at the action timeout if a renderer stuck in a script never
|
|
530
|
+
does.
|
|
531
|
+
A provider that reaches its browser over a network can run captures on a connection of their own,
|
|
532
|
+
a `Browser.CaptureSource` given as `Cdp.Options.capture`, as `effect-browserbase` does: there a
|
|
533
|
+
frame never waits behind a large message on the connection that drives the page. Frames then carry
|
|
534
|
+
the document their own connection saw commit, numbered as `Navigated` numbers them, and a failure
|
|
535
|
+
of that connection ends the capture with `Failed` and leaves the page as it was.
|
|
536
|
+
Concurrent readers share one native screencast and its quality and size: a reader without options
|
|
537
|
+
joins whatever is running, and one whose explicit options differ fails with `InvalidRequest` rather
|
|
538
|
+
than silently receiving other frames. Each reader has a bounded 16-frame queue; a slow reader's
|
|
539
|
+
observed sequence gaps add to `lost`. Late subscribers do not count earlier history.
|
|
540
|
+
ACKs are independent of reader speed and bounded to 32 unresolved replies; exhaustion ends capture
|
|
541
|
+
with a typed error. `recentFrames` keeps the frames painted within `frameHistory` of the newest,
|
|
542
|
+
5 seconds by default, a moment's default window. Capture stops when its last
|
|
543
|
+
reader leaves; a stop whose reply is late is never resent, and the next capture waits for it (up
|
|
544
|
+
to 2 s per attempt) rather than disabling capture for the page.
|
|
545
|
+
|
|
546
|
+
A page's `id` is its CDP target id. A new connection to the same browser, as after a dropped one,
|
|
547
|
+
finds each page under the id it had, with `browser.page(id)`; a stored id the browser no longer
|
|
548
|
+
finds is a page that closed. `browser.pages` lists the open pages in the order the browser began
|
|
549
|
+
tracking them, and `browser.firstPage` gives the first, opening one when there is none.
|
|
550
|
+
|
|
551
|
+
A page's life is told on the browser's timeline. `PageOpened`; `Navigated` for every move of its
|
|
552
|
+
main frame, `sameDocument` for one within the document, such as `pushState`, with `document`
|
|
553
|
+
counting the page's documents from 0, the one it had when the browser began tracking it;
|
|
554
|
+
`PageLoaded` as a document finishes parsing and loading, as the page's own session hears it, so
|
|
555
|
+
after the `Navigated` that began the document; `DialogShown`; and `PageClosed`, by the
|
|
556
|
+
page or because it crashed. A document count belongs to the page within one `Browser`, and starts
|
|
557
|
+
again on a new connection. Each frame carries the document it followed and the page's address. A
|
|
558
|
+
tab the site opened that the library could not track is `PageUntracked`. A tab a page opened names
|
|
559
|
+
it as its `opener`, and the opener's events hold its opening. Chromium announces a title change
|
|
560
|
+
only with the next change of address, so there is no title event: read `page.title` when
|
|
561
|
+
`Navigated` or `PageLoaded` says the page moved. The browser answers it, in one call that a busy
|
|
562
|
+
page cannot hold up, and an untitled page's is empty.
|
|
563
|
+
|
|
564
|
+
The browser's own end is one `Disconnected`, and `browser.disconnected` completes with its cause:
|
|
565
|
+
`connection`, `session`, at or after the provider's `expiresAt`, which `SessionEnding` announces,
|
|
566
|
+
or `released`, by the owner's scope. A browser lost so publishes no `PageClosed` for its pages,
|
|
567
|
+
and calls in flight on them fail at once rather than at their deadline. A screencast's readers are
|
|
568
|
+
told as the page's scope closes, once that cause is known: a capture that fails first, as one on a
|
|
569
|
+
connection of its own can, waits up to two seconds for it before it fails as itself. A crashed page
|
|
570
|
+
is closed: Playwright drives it no more, and its calls would never return.
|
|
571
|
+
|
|
572
|
+
A failure on a page that is gone is `Closed` with its cause: `page`, `crashed`, or the browser's.
|
|
573
|
+
`BrowserError.consequence(error)` says what any failure leaves: what was `lost` (`nothing`, the
|
|
574
|
+
`page`, or the `session` with its pages), and whether to `repeat` the call: `safe`, as nothing
|
|
575
|
+
reached the browser; `check` its effect first, as it may have taken place; `pointless` as it is,
|
|
576
|
+
such as with a stale ref; or `resume` reading events from a newer cursor. `message` is a sentence
|
|
577
|
+
for operators, and the browser tools tell a model what to do about the failure.
|
|
578
|
+
|
|
579
|
+
`Expect` is that check, for an action whose effect the page shows. An attempt names what the action
|
|
580
|
+
is for, `appeared(query)`, `gone(query)`, `changed(query)` or `navigated(url?)`, with `Page.find`'s
|
|
581
|
+
queries over the whole document, and takes its baseline as it begins. Each `run` checks first and
|
|
582
|
+
sends nothing where the expectation holds already, `AlreadyDone`; otherwise it acts and waits for
|
|
583
|
+
it, `Done`, or `NotDone` after `wait`, 5 seconds by default. An action that fails after its input
|
|
584
|
+
went is checked too: `Done` if its effect came, else its own failure, so the next `run` of the same
|
|
585
|
+
attempt, on the same page or the one a reconnect finds by its id, acts only if nothing came.
|
|
936
586
|
|
|
937
587
|
```ts
|
|
938
|
-
|
|
939
|
-
|
|
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) =>
|
|
977
|
-
Effect.gen(function* () {
|
|
978
|
-
yield* session.initialPage.navigate({ url: "https://portal.example.com" });
|
|
979
|
-
yield* session.initialPage.ready();
|
|
980
|
-
return yield* session.initialPage.observe();
|
|
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.
|
|
588
|
+
const placing = yield * Expect.attempt(page, Expect.appeared({ text: "Order placed" }));
|
|
589
|
+
const outcome = yield * placing.run(page, (page) => page.click(ref));
|
|
985
590
|
```
|
|
986
591
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
592
|
+
A check is one call to the page, and so is a wait, which the page answers as the effect comes;
|
|
593
|
+
`navigated` reads what the page already knows, at no call. `changed` compares what the query found
|
|
594
|
+
at the start, its elements' text, field values and states but focus, by a mark the page keeps, so
|
|
595
|
+
none of it leaves the page. On a page a reconnect found, `navigated` without an address tells only
|
|
596
|
+
a move to another address, as the new connection counts documents afresh. A presenter's view is
|
|
597
|
+
checked as its page is; a page a `Browser` did not make is refused.
|
|
598
|
+
|
|
599
|
+
Every address the library reports, in events, frames, reads, errors, a guard's request and the
|
|
600
|
+
outline's links, keeps its identity and loses its userinfo and its credentials' values, which read
|
|
601
|
+
`Page.redacted`. A credential is a query, fragment or path parameter named for a token, secret,
|
|
602
|
+
password, signature, assertion, session id or one-time code, or for a code or key that signs
|
|
603
|
+
someone in, such as `verification_code` or `api_key`. `code`, `key`, `session`, `sid` and `ticket`
|
|
604
|
+
as often name what a page shows, so under them only a value that looks generated, of at least 16
|
|
605
|
+
characters with letters and digits, is one. A chart's `?ticker=ETH` and a quote's `?code=BTC`
|
|
606
|
+
stay; a short one-time code under a bare `code` stays too.
|
|
607
|
+
|
|
608
|
+
`initScripts` take `{ match, source }`. The source runs before each new document's own scripts, in
|
|
609
|
+
every frame, or only where `match`, a `RegExp`, finds the document's address, as a block. Playwright
|
|
610
|
+
registers them as it sets each page up, so a popup's first document runs them too, at no call per
|
|
611
|
+
document.
|
|
612
|
+
|
|
613
|
+
`Browser.events()` streams `RecordedEvent` values: `{ sequence, event }`. The sequence orders
|
|
614
|
+
all browser events and is the replay cursor. Call `browser.events({ after: lastSequence })` to
|
|
615
|
+
resume, or use `after: 0` for everything since the browser opened if it is still retained. With
|
|
616
|
+
no cursor, streaming begins at subscription. `eventHistory` keeps a positive bounded number of
|
|
617
|
+
events (4,096 by default), also exposed without envelopes by `recentEvents`. A reader whose next
|
|
618
|
+
event has expired fails with `EventHistoryExpired`; it never skips events silently. Consume the
|
|
619
|
+
stream promptly and size retention for the consumer’s delay. Closing the browser scope wakes and
|
|
620
|
+
ends idle readers.
|
|
621
|
+
|
|
622
|
+
The presentation track uses the same channel. `TrackPlanned` publishes a whole glide before
|
|
623
|
+
input, with `from` and samples whose `afterMillis` are offsets from `at`. Its sequence identifies
|
|
624
|
+
the plan. `TrackPerformed` ends that plan, names it in `plan`, and records the number of samples
|
|
625
|
+
actually submitted plus the last submitted point. The consumer must trim the planned suffix to
|
|
626
|
+
that count after interruption or failure. `complete` means all samples were submitted; it does
|
|
627
|
+
not claim that the browser acknowledged or painted them.
|
|
628
|
+
|
|
629
|
+
`PointerPressed`, `PointerReleased`, `WheelScrolled`, `KeyChanged`, `TextInserted` and
|
|
630
|
+
`CursorChanged` describe the remaining input. Button, key and text events are published at
|
|
631
|
+
submission, including cleanup releases. Cursor shape comes from resolved target metadata.
|
|
632
|
+
`TrackEvent` is the schema union for these presentation events; a window leaves them out of its
|
|
633
|
+
events. Compositing remains the consumer’s job.
|
|
634
|
+
|
|
635
|
+
A page waits only for itself. Its operations take turns in one lane of its own: an action, which
|
|
636
|
+
sends input or navigates, has the page to itself, in the order actions were asked; reads (the
|
|
637
|
+
outline, `find`, `text`, `changes`, pictures, zooms, `title` and the viewport) share it, after the action in
|
|
638
|
+
flight and every action asked before them, so a read describes the page an action left, never one
|
|
639
|
+
an action is still changing. `ready` follows the action in flight too, and holds none back while it
|
|
640
|
+
watches. An action then waits for its page's unresolved input replies. Its timeout bounds those
|
|
641
|
+
waits before a full timeout bounds the action itself: a turn not given in time fails `Busy`, with
|
|
642
|
+
how long it waited and how many operations were ahead, and replies not answered in time fail
|
|
643
|
+
`Timeout`, as for a page that does not answer. `Page.failFast(effect)` runs operations that fail
|
|
644
|
+
`Busy` at once instead of waiting. A policy hold leaves the page's turn free.
|
|
645
|
+
|
|
646
|
+
Reads keep their work. An identical read asked while one is in flight on the page, with no action
|
|
647
|
+
asked between them, joins it, and costs no call of its own. Each read runs in the page's scope under
|
|
648
|
+
the action timeout whoever gives up, so a caller's own `Effect.timeout` loses nothing: a read that
|
|
649
|
+
ends after all its callers gave up serves the next caller to ask the same within an action timeout,
|
|
650
|
+
unless an action or a new document came first. An action stops the reads nobody awaits rather than
|
|
651
|
+
wait for them. Pictures and `changes` only join: a picture's caller says how old it may be, and a
|
|
652
|
+
window of changes ends as it is read.
|
|
653
|
+
|
|
654
|
+
`page.state` is what the library already knows of a page, at no call and with no wait for its
|
|
655
|
+
turn: where its main frame is, its document and when it was committed, how far that document has
|
|
656
|
+
loaded, its newest screencast frame, and the viewport's text and title as last read in that
|
|
657
|
+
document, each with when it was learned, so a caller reads their ages. A caller that must never
|
|
658
|
+
wait, such as one reading the screen on air, reads it rather than polling the page. Chromium
|
|
659
|
+
announces no title change, so the title is as a read of the text last found it.
|
|
660
|
+
|
|
661
|
+
`Browser.Options.maxPages` bounds the pages a browser keeps open, those a site opened included,
|
|
662
|
+
which are never refused: at the limit, `newPage` waits within the action timeout for one to close,
|
|
663
|
+
then fails `Limit`, or fails at once under `Page.failFast`.
|
|
664
|
+
|
|
665
|
+
Each page has its own pointer, where its last move left it, which is also where Chromium has it:
|
|
666
|
+
its first glide starts mid-viewport, and each later one where the one before ended, whatever
|
|
667
|
+
input on another tab does meanwhile. Every sent move updates the position, including a partially
|
|
668
|
+
cancelled glide. A later viewport clamps the starting point to its bounds if necessary.
|
|
669
|
+
|
|
670
|
+
`Supervisor.make` keeps a browser open across losses and session ends, as generations, each a new
|
|
671
|
+
browser from the provider's `open`. Opens run in the supervisor's own scope, so a caller that stops
|
|
672
|
+
waiting never interrupts a half-open browser. `browser` gives the current generation, waiting up to
|
|
673
|
+
`waitTimeout` while one opens. A lost browser is published at once, `Lost` with its browser's
|
|
674
|
+
`disconnected` cause, and the next one opens on the `reopen` schedule, which also retries a failed
|
|
675
|
+
open until it gives up, unless the provider deems the failure `definite`, such as a refused key:
|
|
676
|
+
then the generation is `Down` at once, with its cause, which `Unavailable` also gives anyone
|
|
677
|
+
waiting, because a schedule would only hide a configuration error. `rotate`, or the time
|
|
678
|
+
`rotateBefore` ahead of a generation's browser's `expiresAt`, opens the next generation before it
|
|
679
|
+
releases the current one, unless generations are `exclusive`: then the current one is released
|
|
680
|
+
first.
|
|
681
|
+
`retire` stops reopening at once and releases what is open, asking the provider's `release` before
|
|
682
|
+
it closes a generation's scope; closing the scope retires too, unless the supervisor `keep`s: then
|
|
683
|
+
the generation serving is left running, `Kept`, its scope closed but its provider never asked to
|
|
684
|
+
release it, for a provider whose scope only disconnects. `states` streams each generation's
|
|
685
|
+
`Opening`, `Reopening`, `Open`, `Lost`, `Down`, `Closed`, the last with the provider's release
|
|
686
|
+
outcome, `Settled` or `Unconfirmed`, and `Kept`, so time open is a subtraction of their stamps.
|
|
687
|
+
Pages don't carry over from one generation to the next. `effect-browserbase`'s
|
|
688
|
+
`Browserbase.supervise` supervises hosted sessions, and with `keep` adopts the one it kept.
|
|
689
|
+
|
|
690
|
+
Every module is also an entry point, such as `effect-browser/Page`. The
|
|
691
|
+
[repository README](https://github.com/mannyc2/effect-agent-browserbase#readme) has examples.
|
|
692
|
+
|
|
693
|
+
## Tracing
|
|
694
|
+
|
|
695
|
+
Operations are spans in Effect's tracer, so an application that installs an exporter sees them in
|
|
696
|
+
its own traces; the library installs none. For the agent in the repository's README, with
|
|
697
|
+
`effect/observability` provided outermost so that opening the browser is traced too:
|
|
1004
698
|
|
|
1005
699
|
```ts
|
|
1006
|
-
import {
|
|
1007
|
-
import
|
|
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).
|
|
700
|
+
import { Effect, Layer } from "effect";
|
|
701
|
+
import { FetchHttpClient } from "effect/http";
|
|
702
|
+
import { Otlp, OtlpSerialization } from "effect/observability";
|
|
1031
703
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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);
|
|
1049
|
-
}),
|
|
1050
|
-
),
|
|
704
|
+
// Exports to OTEL_EXPORTER_OTLP_ENDPOINT when OTEL_TRACES_EXPORTER=otlp is set.
|
|
705
|
+
const Observability = Otlp.layerFromConfig({ resource: { serviceName: "my-agent" } }).pipe(
|
|
706
|
+
Layer.provide(OtlpSerialization.layerJson),
|
|
707
|
+
Layer.provide(FetchHttpClient.layer),
|
|
1051
708
|
);
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
`Capture.multipart(frames)` turns frames into one `multipart/x-mixed-replace` response for an `<img>`, the motion JPEG that WHATWG HTML defines for images. It closes each frame with the next part's delimiter and headers at once, because a browser shows a part only when it has read the headers of the one after it: without that, a viewer runs one frame behind and never shows the last picture of a page that has gone still. It writes no metadata, so nothing but pictures reaches a viewer, and it draws a new boundary from `Crypto` for each response. Call it once per viewer, over one fan-out of the interval, for example a sliding `PubSub` with `replay: 1` so that a slow viewer skips frames without slowing anyone else and a new one is shown the current picture. End that fan-out before the HTTP server stops: a server waits for its open responses.
|
|
1055
|
-
|
|
1056
|
-
Closing, navigating, detaching a relevant frame, or resizing the captured page ends its interval explicitly without ending a sibling page's capture. `Capture.start(page, { lifetime: "page" })` instead follows a page's main frame across documents: start it before a navigation and it covers the loading in between. Same-document URL changes leave document-lifetime capture and observations active. A page-lifetime interval records those changes with `sameDocument: true` and keeps the current document number; only a cross-document commit advances it. The native screencast is never restarted for a navigation, so a boundary is not a gap this package introduced. Each frame carries the `document` it was received during (0, then one more per new document), and the summary's bounded `documentBoundaries` give the last sequence before each URL change and the address it reached, with `initialUrl` for document 0. That is attribution by receipt order, not proof of whose pixels a frame shows: one received just after a navigation can still show the document before it. Selecting another page or frame does not invalidate an unrelated interval. Handoff pause, connection loss, an uncertain owner and session closure still invalidate all child intervals. A confirmed native stop releases only its own reservation; a failed stop on a live page keeps that target quarantined until a late stop confirms or the page definitively closes. Stopping a child capture does not close its browser. The frame seam has **no website-audio source**, so this package does not synthesize silent samples or infer audio support from a video container. Caller encoding is demonstrated in [the caller encoder example](../browserbase/examples/record-video.ts); the example decodes every generated frame with the caller's FFmpeg and checks presentation timestamps and pixel checksums. Native acceptance requires changing pixels and source-time agreement rather than accepting container headers as video evidence. Filming across a navigation with one page-lifetime interval, resampled onto a constant-rate reel with the address of each document reported, is demonstrated in [the footage example](../browserbase/examples/realistic-footage/README.md).
|
|
1057
|
-
|
|
1058
|
-
Same-document detection uses the pinned Playwright 1.63.0 client `Frame` event `navigated`: `newDocument` is absent for URL-only changes, and the client emits this event synchronously before `Page` emits `framenavigated`. A runtime missing that private signal fails explicitly rather than treating URL changes as document commits.
|
|
1059
|
-
|
|
1060
|
-
### Read metadata while capture is running
|
|
1061
|
-
|
|
1062
|
-
```ts
|
|
1063
|
-
const interval = yield * Capture.start(page, { lifetime: "page" });
|
|
1064
|
-
const current = yield * interval.snapshot;
|
|
1065
|
-
// current.phase is "capturing"; initialUrl and recorded documentBoundaries are available now.
|
|
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
709
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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.
|
|
1126
|
-
|
|
1127
|
-
An optional document `documentScroll` supplies an explicit geometry snapshot with the same six fields as a reading. `observe`, `checkpoint` and `document.current` copy those facts; `document.update` supplies a subsequent snapshot. Scripted scroll and resize operations do not infer layout, clamping or RTL origins from those numbers. Omit the field when geometry is unknown. Authored snapshots establish the owner's behavior over those facts, not native layout.
|
|
1128
|
-
|
|
1129
|
-
```ts
|
|
1130
|
-
import { expect, it } from "@effect/vitest";
|
|
1131
|
-
import { Effect } from "effect";
|
|
1132
|
-
import * as Browser from "effect-browser/browser";
|
|
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
|
-
),
|
|
710
|
+
program.pipe(
|
|
711
|
+
Effect.provide([Chromium.layer(), OpenRouter]),
|
|
712
|
+
Effect.provide(Observability),
|
|
713
|
+
Effect.runPromise,
|
|
1180
714
|
);
|
|
1181
715
|
```
|
|
1182
716
|
|
|
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.
|
|
717
|
+
The agent's turns, model calls and tool calls are Yielded's spans, with OpenTelemetry's GenAI
|
|
718
|
+
attributes. Every page operation is a
|
|
719
|
+
`Page.<name>` span, such as `Page.click`, `Page.type` or `Page.navigate`, with what its `Action`
|
|
720
|
+
records except text: the target, the subject's role, name and tag, whether input was dispatched,
|
|
721
|
+
`queuedMillis` spent waiting for its turn and the page's replies, and a failure's reason as
|
|
722
|
+
`error.type`. A read that shared another's work says so in `shared`: `joined` or `kept`.
|
|
723
|
+
`Page.prepare`, the policy's preparation, and `Page.guard`, which lasts as long as a hold and holds
|
|
724
|
+
a judge's model call, are its children, as are pointer travel (`Page.move`) and the settle after input (`Page.settle`).
|
|
725
|
+
`Page.snapshot`, `Page.find`, `Page.text`, `Page.title`, `Page.viewport`,
|
|
726
|
+
`Page.screenshot` and `Page.frame` (with
|
|
727
|
+
`source`: a reused screencast `frame` or a new `screenshot`), `Page.zoom`, `Page.ready` and the
|
|
728
|
+
other waits are spans, as are `Plan.replay` and `Plan.locate`, and so is each round trip to the
|
|
729
|
+
page's script (`Page.evaluate`) and its registration (`Page.register`). Each page span reports its cost on the page's own protocol session: `calls`,
|
|
730
|
+
`bytesOut` and `bytesIn` (their parameters and results as JSON) and `waitedMillis`, how long at
|
|
731
|
+
least one call awaited its reply. An operation inside another counts toward both. Calls Playwright
|
|
732
|
+
makes on its own sessions, such as navigation, are not counted. `Chromium.launch`, `Cdp.connect` and
|
|
733
|
+
`Browser.newPage` cover opening a browser; `Browserbase.open` records its session's id and region,
|
|
734
|
+
`Browserbase.release` a release and its outcome, and `Browserbase.holdContext` the wait for a
|
|
735
|
+
stored context another session writes to. `Page.calibrateClock` maps the browser's clock onto the
|
|
736
|
+
host's with three probes, each a `Page.evaluate` of `clock`, and records the fastest answered
|
|
737
|
+
probe's `roundTripMillis`: the round trip to the browser, plus any wait behind the page's own work.
|
|
738
|
+
One that runs alongside a capture shows its failure only there.
|
|
739
|
+
|
|
740
|
+
Page spans never carry typed text, and a script round trip is named by its function alone. A tool
|
|
741
|
+
call's parameters, which hold what a model typed, are Yielded's to record, under its own telemetry
|
|
742
|
+
and redaction. `Page.evaluate` spans are at `Trace` level and the phases
|
|
743
|
+
inside an action at `Debug`: set `Tracer.MinimumTraceLevel` to `"Info"` to keep only the coarser
|
|
744
|
+
spans.
|