effect-browserbase 0.1.0-beta.104 → 0.2.0-beta.0
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 +61 -305
- package/dist/Account.d.mts +4 -6
- package/dist/Account.mjs +4 -9
- package/dist/Account.mjs.map +1 -1
- package/dist/{Acquisition-WBafHwXH.mjs → Acquisition-D8o4jtCg.mjs} +22 -68
- package/dist/Acquisition-D8o4jtCg.mjs.map +1 -0
- package/dist/Agents.d.mts +4 -6
- package/dist/Agents.mjs +3 -19
- package/dist/Agents.mjs.map +1 -1
- package/dist/Allocation.d.mts +9 -11
- package/dist/Allocation.mjs +2 -4
- package/dist/Allocation.mjs.map +1 -1
- package/dist/Browser.d.mts +23 -141
- package/dist/Browser.mjs +208 -164
- package/dist/Browser.mjs.map +1 -1
- package/dist/BrowserBinding.d.mts +7 -34
- package/dist/BrowserBinding.mjs +30 -1
- package/dist/BrowserBinding.mjs.map +1 -0
- package/dist/Certificates.d.mts +3 -5
- package/dist/Certificates.mjs +2 -7
- package/dist/Certificates.mjs.map +1 -1
- package/dist/{Cleanup-G2YpTCuU.d.mts → Cleanup-DMTDBir8.d.mts} +5 -6
- package/dist/Cleanup.d.mts +3 -2
- package/dist/Cleanup.mjs +1 -6
- package/dist/Cleanup.mjs.map +1 -1
- package/dist/{Client-D1_xYM9o.mjs → Client-BQteUePT.mjs} +2 -4
- package/dist/{Client-D1_xYM9o.mjs.map → Client-BQteUePT.mjs.map} +1 -1
- package/dist/Client.d.mts +51 -2
- package/dist/Client.mjs +1 -1
- package/dist/Connection-DcvJZEdc.mjs +65 -0
- package/dist/Connection-DcvJZEdc.mjs.map +1 -0
- package/dist/ContextCoordination.d.mts +5 -6
- package/dist/ContextCoordination.mjs +1 -8
- package/dist/ContextCoordination.mjs.map +1 -1
- package/dist/Contexts.d.mts +4 -6
- package/dist/Contexts.mjs +2 -8
- package/dist/Contexts.mjs.map +1 -1
- package/dist/Downloads.d.mts +6 -8
- package/dist/Downloads.mjs +3 -8
- package/dist/Downloads.mjs.map +1 -1
- package/dist/Errors.d.mts +3 -26
- package/dist/Errors.mjs +1 -137
- package/dist/Errors.mjs.map +1 -1
- package/dist/{Extensions-ClZbYb1H.mjs → Extensions-nMNMPGCc.mjs} +3 -9
- package/dist/{Extensions-ClZbYb1H.mjs.map → Extensions-nMNMPGCc.mjs.map} +1 -1
- package/dist/Extensions.d.mts +4 -6
- package/dist/Extensions.mjs +1 -1
- package/dist/Functions.d.mts +4 -6
- package/dist/Functions.mjs +3 -17
- package/dist/Functions.mjs.map +1 -1
- package/dist/{Launch-BHcAEq05.mjs → Launch-m0fxzJ49.mjs} +5 -5
- package/dist/Launch-m0fxzJ49.mjs.map +1 -0
- package/dist/Launch.d.mts +130 -2
- package/dist/Launch.mjs +4 -11
- package/dist/Launch.mjs.map +1 -1
- package/dist/PageFetch.d.mts +3 -5
- package/dist/PageFetch.mjs +2 -8
- package/dist/PageFetch.mjs.map +1 -1
- package/dist/Projects.d.mts +3 -5
- package/dist/Projects.mjs +2 -8
- package/dist/Projects.mjs.map +1 -1
- package/dist/Recordings.d.mts +6 -8
- package/dist/Recordings.mjs +3 -5
- package/dist/Recordings.mjs.map +1 -1
- package/dist/{References-URDAtapt.d.mts → References-DOH3Wu44.d.mts} +5 -6
- package/dist/References.d.mts +3 -2
- package/dist/References.mjs +10 -18
- package/dist/References.mjs.map +1 -1
- package/dist/Replays.d.mts +6 -8
- package/dist/Replays.mjs +3 -5
- package/dist/Replays.mjs.map +1 -1
- package/dist/Search.d.mts +3 -5
- package/dist/Search.mjs +2 -9
- package/dist/Search.mjs.map +1 -1
- package/dist/{SessionData-DRKcKCqw.d.mts → SessionData-5xYZDshJ.d.mts} +5 -6
- package/dist/SessionData.d.mts +3 -2
- package/dist/SessionData.mjs +1 -8
- package/dist/SessionData.mjs.map +1 -1
- package/dist/{Sessions-CtbseWEh.mjs → Sessions-DoQGg9nB.mjs} +34 -18
- package/dist/Sessions-DoQGg9nB.mjs.map +1 -0
- package/dist/{Sessions-B8E4kMxo.d.mts → Sessions-tFJYaXFq.d.mts} +8 -8
- package/dist/Sessions.d.mts +4 -3
- package/dist/Sessions.mjs +1 -1
- package/dist/{Transfers-Czv5IZzJ.d.mts → Transfers-GVqzrDWl.d.mts} +22 -9
- package/dist/Transfers.d.mts +3 -2
- package/dist/Transfers.mjs +12 -28
- package/dist/Transfers.mjs.map +1 -1
- package/dist/Uploads.d.mts +6 -8
- package/dist/Uploads.mjs +3 -5
- package/dist/Uploads.mjs.map +1 -1
- package/dist/Webhooks.d.mts +3 -5
- package/dist/Webhooks.mjs +2 -11
- package/dist/Webhooks.mjs.map +1 -1
- package/dist/{WriterFacts-osvYAsIx.d.mts → WriterFacts-5CPBGG1P.d.mts} +4 -3
- package/dist/index.d.mts +27 -31
- package/dist/index.mjs +26 -31
- package/package.json +3 -28
- package/dist/Acquisition-WBafHwXH.mjs.map +0 -1
- package/dist/Association-CEps4hYP.mjs +0 -10
- package/dist/Association-CEps4hYP.mjs.map +0 -1
- package/dist/Bootstrap-BXuzZojd.mjs +0 -194
- package/dist/Bootstrap-BXuzZojd.mjs.map +0 -1
- package/dist/Bootstrap-m7lHgj0D.d.mts +0 -177
- package/dist/Bootstrap.d.mts +0 -2
- package/dist/Bootstrap.mjs +0 -2
- package/dist/BrowserBinding-DlJTGYZL.mjs +0 -2508
- package/dist/BrowserBinding-DlJTGYZL.mjs.map +0 -1
- package/dist/BrowserData-LpcAuAub.d.mts +0 -369
- package/dist/BrowserData.d.mts +0 -2
- package/dist/BrowserData.mjs +0 -442
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/Capture-Bc0z9IAi.mjs +0 -571
- package/dist/Capture-Bc0z9IAi.mjs.map +0 -1
- package/dist/Capture-BvWI-l1A.d.mts +0 -168
- package/dist/Capture.d.mts +0 -2
- package/dist/Capture.mjs +0 -2
- package/dist/Client-Ca0zSg1n.d.mts +0 -53
- package/dist/Images-DgscEfSf.mjs +0 -127
- package/dist/Images-DgscEfSf.mjs.map +0 -1
- package/dist/Launch-BHcAEq05.mjs.map +0 -1
- package/dist/Launch-C8CfaYxb.d.mts +0 -132
- package/dist/LocalBrowser-BLancJ3R.d.mts +0 -79
- package/dist/LocalBrowser-DsQLWNL1.mjs +0 -439
- package/dist/LocalBrowser-DsQLWNL1.mjs.map +0 -1
- package/dist/LocalBrowser.d.mts +0 -2
- package/dist/LocalBrowser.mjs +0 -2
- package/dist/PageControl.d.mts +0 -16
- package/dist/PageControl.mjs +0 -36
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
- package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
- package/dist/Session-D4nT9hOO.mjs +0 -1379
- package/dist/Session-D4nT9hOO.mjs.map +0 -1
- package/dist/Sessions-CtbseWEh.mjs.map +0 -1
- package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Browserbase for Effect
|
|
2
2
|
|
|
3
|
-
`effect-browserbase`
|
|
3
|
+
`effect-browserbase` supplies Browserbase resources and hosted lifetimes to the shared browser runtime: account identity and transport, session, context and extension resources, one owned browser over Playwright/CDP — allocated or borrowed — with trusted registrations, modeled file selection, bounded live capture, explicit page holds, the provider's recordings, replays, uploads and website downloads, and the platform APIs outside a browser session (projects, certificates, Search, Fetch, Agents, Functions and webhooks).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Its host supplies the required `effect-browser` peer, at the same coordinated `0.2.0-beta.0` version for this candidate; it has no Effect Agent dependency. Install both packages explicitly so the provider and application share one runtime instance. Playwright is an optional peer of the shared runtime and is loaded only when a browser connects; artifact-only consumers do not need it. The existing Effect peer range remains `^4.0.0-rc.115`, with only rc.115 qualified by this candidate. The package targets trusted Node and Bun hosts. It is an unpublished maintainer-review candidate: hosted provider behavior requires separate validation, and ordinary tests never create a paid Browserbase session or invoke a paid model.
|
|
6
6
|
|
|
7
7
|
## One account, then resources
|
|
8
8
|
|
|
@@ -35,7 +35,8 @@ For the simplest post-session video path, opt in to Browserbase recording in the
|
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
37
|
import { BrowserbaseBrowser } from "effect-browserbase/browser";
|
|
38
|
-
import
|
|
38
|
+
import * as Browser from "effect-browser/browser";
|
|
39
|
+
import { BrowserPolicy, NavigateRequest } from "effect-browser/browser-data";
|
|
39
40
|
import { recipe } from "effect-browserbase/launch";
|
|
40
41
|
import { BrowserbaseRecordings } from "effect-browserbase/recordings";
|
|
41
42
|
import { RecordingPageReference } from "effect-browserbase/transfers";
|
|
@@ -52,16 +53,13 @@ const layers = Layer.merge(BrowserbaseBrowser.layer({ launch }), BrowserbaseReco
|
|
|
52
53
|
);
|
|
53
54
|
|
|
54
55
|
const program = Effect.gen(function* () {
|
|
55
|
-
const browser = yield* BrowserbaseBrowser;
|
|
56
56
|
const recordings = yield* BrowserbaseRecordings;
|
|
57
57
|
|
|
58
|
-
const reference = yield*
|
|
58
|
+
const reference = yield* Browser.scoped(BrowserbaseBrowser.open(policy), (session) =>
|
|
59
59
|
Effect.gen(function* () {
|
|
60
|
-
|
|
60
|
+
yield* session.navigate(NavigateRequest.make({ url: "https://example.com" }));
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// An AgentRuntime can borrow this same session through the adapter package.
|
|
62
|
+
// Browser.scoped checks the owner's release before returning this durable identity.
|
|
65
63
|
return session.reference;
|
|
66
64
|
}),
|
|
67
65
|
);
|
|
@@ -91,7 +89,7 @@ The launch recipe deliberately differs from three Browserbase defaults. Set a fi
|
|
|
91
89
|
| `logSession` | on | off | CDP logs carry page content and typed input |
|
|
92
90
|
| `solveCaptchas` | on | off | solving is an action the host should choose |
|
|
93
91
|
|
|
94
|
-
For model-driven control, use `effect-agent-
|
|
92
|
+
For model-driven control, use `effect-agent-browser`, whose tools borrow an already-owned session rather than allocating a browser per tool call. Provider credentials, CDP URLs, context choices, Live View controls and recording configuration are host decisions and are never Tool parameters.
|
|
95
93
|
|
|
96
94
|
## Choose a video path
|
|
97
95
|
|
|
@@ -120,204 +118,30 @@ That split is why an agent can use one browser for many turns while provider art
|
|
|
120
118
|
|
|
121
119
|
`Allocation.scoped(recipe)` allocates a session without connecting a browser, for a caller that drives the remote session with its own automation client. It is the same allocation path the browser owner uses, so the creation request is never retried, an unknown outcome is still reported exactly once, a persisting Context still needs a live writer permit, and no native peer is loaded. The scope owns the release: closing it releases the session whether or not anything ever connected. A reply naming another project is refused, and because a foreign reference is never acted on, the caller is handed that reference rather than a mutation attempted against it.
|
|
122
120
|
|
|
123
|
-
The browser
|
|
124
|
-
|
|
125
|
-
Mutations are serialized. An observation identifies retained native nodes only until the next invalidating event; it is not a DOM snapshot version. Replaced or detached nodes fail instead of silently resolving to replacements. An action interrupted or timed out after native dispatch has an **unknown** outcome: the owner is fenced and the package never automatically replays it. `undispatched` is used only when the package established that native mutation dispatch did not occur.
|
|
126
|
-
|
|
127
|
-
### A navigation you can watch while it loads
|
|
128
|
-
|
|
129
|
-
`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:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
const operation =
|
|
133
|
-
yield * handle.startNavigation(StartNavigationRequest.make({ url, timeoutMillis: 30_000 }));
|
|
134
|
-
|
|
135
|
-
const early = yield * session.checkpoint({ picture: true }); // what has loaded so far
|
|
136
|
-
const receipt = yield * PageControl.suspend(session, page); // timers, CSS and parsing stop
|
|
137
|
-
yield * PageControl.resume(session, receipt);
|
|
138
|
-
const { url: loaded } = yield * operation.completed;
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
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.
|
|
142
|
-
|
|
143
|
-
- `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.
|
|
144
|
-
- `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.
|
|
145
|
-
- Leaving the operation's scope unsettled, or a navigation that fails after dispatch, fences the session as uncertain, exactly as an interrupted mutation always has. Nothing knows what the browser did, so nothing more is sent, and it is never replayed.
|
|
146
|
-
|
|
147
|
-
A read is ordered against the document being replaced by failing: if the document it was reading was replaced underneath it, or a navigation is still in flight on its page, the error is `target-changed` and `undispatched`, which means read again. A read is never a mutation, so that is always safe. A held page is not read at all; take the checkpoint before the hold and keep it.
|
|
148
|
-
|
|
149
|
-
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.
|
|
150
|
-
|
|
151
|
-
### What is on screen, and what a host may know about it
|
|
152
|
-
|
|
153
|
-
`session.observe()` reads the whole document. `session.observe({ scope: "viewport" })` keeps only text and controls that are on screen and reachable, and says what it left out:
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
const seen = yield * session.observe({ scope: "viewport" });
|
|
157
|
-
seen.viewport; // { clippedText, coveredText, uncertainText, unreachableControls, exhausted, ... }
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Visibility here is geometry and hit-testing, never a pixel comparison, and the counts say which was which. Text is kept when its line boxes intersect the viewport and the browser finds its own element at a sampled point. A text node that crosses the viewport edge contributes only its lines on screen (`clippedText`). Text behind another element is left out (`coveredText`). Text under something that takes no pointer events cannot be hit-tested at all, so it is left out as `uncertainText` rather than called visible. `exhausted` means the traversal budget ran out first and the reading is known to be incomplete. Canvas pixels and compositing effects are not interpreted.
|
|
161
|
-
|
|
162
|
-
An `Observation` is safe to show a model, and the adapter's `browser_inspect` Tool returns it as is. It therefore carries no destination, form target or field value, in either scope. What a host needs to decide whether a control may be acted on is a separate, host-only read from the exact node:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
const facts = yield * session.controlFacts(reference);
|
|
166
|
-
// kind, label, disabled, editable, inputType, autocomplete, formMethod, box, placement, hitTest
|
|
167
|
-
// destination: the resolved link target, or where this control submits its form
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
`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.
|
|
171
|
-
|
|
172
|
-
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`, exactly as it does for a replaced or detached node. Nothing is ever re-found by selector or label.
|
|
173
|
-
|
|
174
|
-
When a decision has to be current at dispatch, pass a policy. It is evaluated on facts read from that exact node immediately before the input:
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
yield *
|
|
178
|
-
session.fillElement(reference, value, {
|
|
179
|
-
admit: (facts) => facts.inputType !== "password" && facts.autocomplete !== "cc-number",
|
|
180
|
-
});
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Anything but `true`, or a policy that throws, sends nothing and fails `denied`. The policy is a plain synchronous function on purpose: it runs while the owner's permit is held, where waiting on a model or a network call would stall every other operation. It is not an atomic check-and-input transaction, because page script can still run before the native input lands.
|
|
184
|
-
|
|
185
|
-
### Passive checkpoints for a recorder
|
|
186
|
-
|
|
187
|
-
`observe()` replaces the one observation whose nodes later actions may name, so a recorder calling it would retire the references an agent is about to use. `session.checkpoint()` is the passive path: viewport text, control facts and, when asked, a PNG of the viewport.
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
const checkpoint = yield * session.checkpoint({ picture: true });
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
It issues no references, is not a mutation, and leaves the action observation and the selection exactly as they were, so inspect, checkpoint, then act on the inspected node all compose. It is host-only, because it carries control facts. Text and picture are read one after the other, never atomically: the interval is on the host monotonic clock, and `documentChanged` says the document was replaced in between. A checkpoint is charged as one action, like any other bounded read. A held page is refused `busy` rather than woken to be read: take the checkpoint before the hold and keep it.
|
|
194
|
-
|
|
195
|
-
### Real pointer and wheel input
|
|
196
|
-
|
|
197
|
-
`pointerMove`, `hover` and `wheel` send the input a person's hardware would, so pages see trusted events, `:hover` applies, and the browser itself decides what is under the pointer. `scroll` stays what it was: script in the page, instantaneous, raising no wheel event. That difference is how a recording tells one from the other.
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
const handle = session.bind();
|
|
201
|
-
|
|
202
|
-
yield * handle.pointerMove(PointerMoveRequest.make({ to: { x: 140, y: 100 } }));
|
|
203
|
-
yield * handle.hover(HoverRequest.make({ selector: "#menu" }));
|
|
204
|
-
// A nested scroll container under the pointer scrolls, not the page.
|
|
205
|
-
const receipt =
|
|
206
|
-
yield * handle.wheel(WheelRequest.make({ deltaX: 0, deltaY: 240, at: { x: 420, y: 120 } }));
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Coordinates are CSS pixels in the main frame's viewport. Each call is one native command, charged as one action and fenced like any other mutation, so a handle bound to a page that is no longer selected sends nothing to either page. Easing, pacing and cursor artwork are yours: send the points you want, and draw the cursor from the positions the receipts report.
|
|
210
|
-
|
|
211
|
-
`hover` places the pointer on one exact element where it is, by selector or by the node an observation named (`session.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`.
|
|
212
|
-
|
|
213
|
-
For a child-frame element, hover checks the commanded point through each ancestor
|
|
214
|
-
frame and then the exact node, including cross-origin documents. The receipt
|
|
215
|
-
still uses main-viewport CSS coordinates. An overlay or clipping ancestor refuses
|
|
216
|
-
input. Frame traversal is bounded at 32 levels, with bounded shadow/DOM ancestry
|
|
217
|
-
checks. Axis-aligned translation and positive scaling are supported; rotation,
|
|
218
|
-
perspective and other unsupported frame mappings are refused rather than guessed.
|
|
219
|
-
These reads and native input are separate operations in the browser: page script
|
|
220
|
-
can still change geometry between validation and dispatch.
|
|
221
|
-
|
|
222
|
-
An `InputReceipt` carries the target it was sent to, the position this owner commanded (null until it has placed the pointer on that page), and an interval on the same host monotonic clock that stamps `CapturedFrame.receivedMonotonicNanos`. Input and pixels share one timeline, so a compositor can place the pointer on the frame that shows it. A wheel event is dispatched, not awaited: the receipt does not claim the page finished scrolling or that any frame shows it.
|
|
223
|
-
|
|
224
|
-
### Real key input
|
|
225
|
-
|
|
226
|
-
`fill` sets a field's value in one step: the page gets an `input` event and no `keydown`, `keypress` or `keyup`, so anything that reacts to keys behaves differently under a recorder than it does for a person. `press` and `type` send the strokes a keyboard would. Handlers see trusted key events, and the browser does what it does for a person: Tab moves focus and selects the field it lands in, Enter submits a form that has a submit button, and Backspace edits.
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
const handle = session.bind();
|
|
230
|
-
|
|
231
|
-
// Focus is the page's business. A real click gives it, and keys then follow it.
|
|
232
|
-
yield * handle.click(ClickRequest.make({ selector: "#from" }));
|
|
233
|
-
yield * handle.type(TypeRequest.make({ text: "Vienna" }));
|
|
234
|
-
yield * handle.press(PressRequest.make({ key: "Backspace" }));
|
|
235
|
-
yield * handle.press(PressRequest.make({ key: "k", modifiers: ["Control"] }));
|
|
236
|
-
// Sent only if `#from` still has focus; otherwise nothing is sent at all.
|
|
237
|
-
yield * handle.press(PressRequest.make({ key: "Enter", into: "#from" }));
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Keys go to whatever has focus in the selected page, in whichever frame that is, because that is where the browser sends them. `into` narrows that to one exact element, which must already have focus or hold the element that does, through any open shadow root. If it does not, the call fails `not-focused` and `undispatched`. It never focuses the element for you, for the reason `hover` never scrolls: that would hide a scripted focus inside a native-input operation. `session.pressElement` and `session.typeElement` apply the same rule to the node an observation named and take the same host admission as `fillElement`, so switching from `fill` to real typing gives up neither exactness nor the check on fresh control facts.
|
|
121
|
+
The shared browser runtime delegates allocation and release to this provider lifetime. It supplies the local half of cleanup — fence, capture stop, initialization teardown, disconnect — and the canonical control plane owns the release request and the terminal status observation. A local disconnect, an accepted provider release request and provider-confirmed termination therefore stay distinct facts in one `CleanupResult`, together with the exact steps that failed.
|
|
241
122
|
|
|
242
|
-
|
|
123
|
+
`onCleanup` runs once after the canonical receipt and any Context-writer settlement are recorded.
|
|
124
|
+
Its construction throws, defects, self-interruption and two-second cooperative timeout are
|
|
125
|
+
contained separately from the built-in diagnostics reporter. A failed reporter cannot suppress
|
|
126
|
+
the sink, and a failed sink cannot change cleanup facts or repeat release. The receipt remains
|
|
127
|
+
readable through `cleanupResult`; `closeChecked` still fails when its actual confirmation predicate
|
|
128
|
+
fails. An outer race can discard that checked error, so keep any required host receipt sink outside
|
|
129
|
+
the raced workflow. The shared guide's [Layer and receipt examples](../browser/README.md#a-long-lived-session-in-a-layer)
|
|
130
|
+
describe sharing one bounded session and the supervision that a Layer alone does not provide.
|
|
243
131
|
|
|
244
|
-
|
|
132
|
+
Mutations are serialized. An observation identifies retained native nodes only until the next invalidating event; it is not a DOM snapshot version. Replaced or detached nodes fail instead of silently resolving to replacements. An action interrupted or timed out after native dispatch has an **unknown** outcome and is never automatically replayed. Unresolved control fences the owner; the shared runtime's main-frame navigation recovery can preserve usability after one acknowledged stop while still reporting `Timeout/unknown`. Recovery is bounded by the loading deadline plus at most three seconds and the existing lifetime. Child-frame timeouts retain the conservative fence. `undispatched` is used only when the package established that native mutation dispatch did not occur.
|
|
245
133
|
|
|
246
|
-
|
|
134
|
+
## Shared browser operations
|
|
247
135
|
|
|
248
|
-
|
|
136
|
+
Navigation, observation, native input, typed bootstrap bindings, live capture and page holds are implemented by [`effect-browser`](../browser/README.md). Import their contracts from that package and use them with the exact `BrowserbaseSession` returned here. The [`effect-agent-browser`](../agent-browser/README.md) adapter and maintained tools accept both this session and self-managed Chromium without a second connection.
|
|
249
137
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
```ts
|
|
259
|
-
import * as Bootstrap from "effect-browserbase/bootstrap";
|
|
260
|
-
import { BrowserbaseBrowser } from "effect-browserbase/browser";
|
|
261
|
-
import { BrowserPolicy } from "effect-browserbase/browser-data";
|
|
262
|
-
import { Context, Effect, Schema } from "effect";
|
|
263
|
-
|
|
264
|
-
class ShowSettings extends Context.Service<ShowSettings, { readonly title: string }>()(
|
|
265
|
-
"ShowSettings",
|
|
266
|
-
) {}
|
|
267
|
-
|
|
268
|
-
const bootstrap = Bootstrap.combine(
|
|
269
|
-
Bootstrap.permissions({ origin: "https://portal.example.com", permissions: ["clipboard-read"] }),
|
|
270
|
-
Bootstrap.binding({
|
|
271
|
-
name: "getShowSettings",
|
|
272
|
-
origins: ["https://portal.example.com"],
|
|
273
|
-
input: Schema.Struct({ version: Schema.Literal(3) }),
|
|
274
|
-
output: Schema.Struct({ title: Schema.String }),
|
|
275
|
-
maxConcurrent: 2,
|
|
276
|
-
maxInputBytes: 256,
|
|
277
|
-
maxOutputBytes: 4096,
|
|
278
|
-
timeoutMillis: 2000,
|
|
279
|
-
failureMode: "fail-session",
|
|
280
|
-
handle: () => Effect.map(ShowSettings, (settings) => ({ title: settings.title })),
|
|
281
|
-
}),
|
|
282
|
-
Bootstrap.init({
|
|
283
|
-
id: "show-settings-v3",
|
|
284
|
-
origins: ["https://portal.example.com"],
|
|
285
|
-
content: "globalThis.__ready = globalThis.getShowSettings({ version: 3 }).then(() => true);",
|
|
286
|
-
readiness: {
|
|
287
|
-
expression: "globalThis.__ready",
|
|
288
|
-
timeoutMillis: 5_000,
|
|
289
|
-
existingDocuments: "RequireFreshNavigation",
|
|
290
|
-
},
|
|
291
|
-
}),
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
const run = Effect.gen(function* () {
|
|
295
|
-
const browser = yield* BrowserbaseBrowser;
|
|
296
|
-
return yield* browser.withBrowser(BrowserPolicy.unrestricted(), { bootstrap }, (session) =>
|
|
297
|
-
Effect.gen(function* () {
|
|
298
|
-
yield* session.bind().navigate({ url: "https://portal.example.com" });
|
|
299
|
-
yield* session.ready;
|
|
300
|
-
return yield* session.observe();
|
|
301
|
-
}),
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
// run requires BrowserbaseBrowser and ShowSettings. withBrowser discharges both the
|
|
305
|
-
// callback's and the use function's Scope, but neither one's other services or errors.
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Registration is installed before this connection creates any document, and permissions precede the bundle. It is still not readiness: an asynchronous step cannot pause a website's own scripts, so a document is ready only when its expression resolves to exactly `true`. Readiness is keyed by frame and document epoch and evaluated once per document, so a completed wait can never ready the document that replaced the one it observed.
|
|
309
|
-
|
|
310
|
-
Each binding accepts exactly one JSON-compatible argument and returns the output codec's **encoded** JSON value. A transforming codec such as `Schema.FiniteFromString` therefore exposes a string to the page while its host handler works with a number. The native callback validates the actual caller's allowed origin and document before input decoding, immediately before invoking the handler, and before replying. This uses Chromium's execution-context identity and `uniqueContextId` on child CDP sessions belonging to the existing connection: a frame URL, a page-supplied origin, and a reused numeric context id are not authorization. The pinned Playwright `exposeBinding` callback supplies a frame but not the calling document's identity; it is deliberately not used as a weaker substitute. No raw protocol or second browser owner is exposed.
|
|
311
|
-
|
|
312
|
-
Plans admit at most 16 uniquely named bindings. Each binding declares its concurrent-call, UTF-8 input/output byte and whole-invocation deadline limits. Admission reserves capacity before native validation, codec work or a callback fiber starts; a timed-out native operation retains that reservation until it actually settles, including across reconnect. The page wrapper additionally rejects cyclic, sparse, accessor-bearing, non-plain, non-finite and non-JSON input rather than silently changing it through `JSON.stringify`. Its traversal admits at most 64 levels and 65,536 nodes; the configured byte limit still applies. Native target and default-document registries are finite, and closed native targets retire their authority immediately.
|
|
313
|
-
|
|
314
|
-
`reject-call` rejects only the affected invocation and permits subsequent healthy calls. `fail-session` completes `session.failure` with the original typed consumer cause and fences the owner. Pages receive only `BrowserbaseBindingError: Browser binding call rejected`, with no host stack, consumer error payload, credentials or SDK cause. `session.bindingDiagnostics` is a bounded **host-only** snapshot containing per-binding accounting and the latest 32 typed causes; do not serialize it into a Tool response. Callback service reads run independently of a browser mutation, but reentrant browser work never waits behind that mutation's permit: it fails `busy` with `undispatched` instead.
|
|
315
|
-
|
|
316
|
-
`withBrowser` races the use function against fail-session errors, discharges the scope, and permits normal success only after confirmed owned-session termination and complete local cleanup. Explicit `acquire`/`open` retain the typed failure signal and detailed cleanup receipt for callers that need to manage that decision themselves. Teardown synchronously closes callback admission, interrupts managed callback fibers, removes this connection's registrations, disconnects locally, and still attempts remote release if a prior cleanup step fails. Reconnect installs fresh callable registrations, never replays an old invocation or a consumer init script into an already-running document, and cannot reuse quarantined callback capacity.
|
|
317
|
-
|
|
318
|
-
Operations that depend on an initialized document wait for the current one. Navigation, selection and page management do not, so initialization cannot deadlock the navigation that produces the document it is waiting for. A document that was already running when the bundle was registered — the page you attach to, or the one a reconnect finds — never ran it: `RequireFreshNavigation` reports `RequiresNavigation` and refuses dependent work, while `AcceptAlreadyRunning` verifies the requirement against that document instead of assuming it. Neither reloads a page whose work may be uncertain; that stays your decision. `session.ready` reports the current document without charging an action, and an origin outside the plan is reported as `NotApplicable` rather than waited on.
|
|
319
|
-
|
|
320
|
-
The reviewed permission subset is granted against a real browser in native acceptance rather than copied from a list. Extension identity and storage across Context reuse, and registration retention across provider reconnects, remain hosted questions and are not claimed here.
|
|
138
|
+
The shared session also provides passive `status` and bounded `diagnostics`, including after
|
|
139
|
+
closure. Known expiry, callback failure and policy pressure are not automatically labeled
|
|
140
|
+
uncertain; unresolved native dispatch remains a separate fact. Owned provider termination can
|
|
141
|
+
retire native control, whereas borrowed disconnection alone cannot. These snapshots do not replace
|
|
142
|
+
the provider's canonical cleanup receipt or make business outcomes known. `maxHostReads` is a
|
|
143
|
+
browser Layer option (default 10,000, maximum 1,000,000) for checkpoint/control-facts sampling;
|
|
144
|
+
it is independent of model action accounting and never a tool parameter.
|
|
321
145
|
|
|
322
146
|
## Files in and out
|
|
323
147
|
|
|
@@ -329,13 +153,41 @@ Larger files use `BrowserbaseUploads.create`, which places bytes for the exact r
|
|
|
329
153
|
|
|
330
154
|
`BrowserbaseBrowser.attach(reference, { policy, target })` takes control of a session this process did not allocate. It reads status first, so project authority is checked before anything is borrowed; a terminal session is refused because that needs a fresh allocation rather than a reattachment, and a starting session is admitted only within a bounded `pendingWaitMillis`. The requested target is resolved explicitly — there is no positional first-tab fallback — and connection credentials are fetched fresh, because expiry races the caller's own status read.
|
|
331
155
|
|
|
332
|
-
A borrowed scope disconnects locally and reports `ownership: "borrowed"` with `remote: "not-owned"`. It never requests release and never claims Context-writer authority: whoever allocated the session keeps both. It also does not detach and reattach inside itself; attaching again is the cross-process path, and it revalidates the session instead of assuming it is still there. A prior uncertain business mutation is still yours to reconcile before the next one; nothing is replayed automatically.
|
|
156
|
+
`session.closeChecked` accepts confirmed owned cleanup or complete borrowed disconnection according to the actual ownership, then returns the same frozen `CleanupResult` cached by `close`. The concrete success value replaces the former `undefined`; failed confirmation remains a typed `BrowserError`. A borrowed scope disconnects locally and reports `ownership: "borrowed"` with `remote: "not-owned"`. It never requests release and never claims Context-writer authority: whoever allocated the session keeps both. It also does not detach and reattach inside itself; attaching again is the cross-process path, and it revalidates the session instead of assuming it is still there. A prior uncertain business mutation is still yours to reconcile before the next one; nothing is replayed automatically.
|
|
157
|
+
|
|
158
|
+
## Shared API migration
|
|
159
|
+
|
|
160
|
+
| Before | Now |
|
|
161
|
+
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `session.bind()` or `yield* session.currentTarget` | Ordinary calls use `session` directly; `yield* session.retain` explicitly obtains checked stale-on-selection semantics |
|
|
163
|
+
| Bare page IDs in selection/closure, selection returns a handle | Pass `PageInfo` to `selectPage`/`closePage`; selection returns `void`, and `createPage` returns the exact created record |
|
|
164
|
+
| Reuse old page/frame metadata after reconnect | In the same known browser lifetime, read fresh pages and match exactly one saved `targetId`; then read fresh frames. No fallback to order, old ID, URL or title |
|
|
165
|
+
| `Adapter.fromSession(session)` or `currentHandle` | Await the Effect with explicit `{ selection: "current" }` or `{ selection: "retained" }`; its browser is the original concrete session |
|
|
166
|
+
| `BrowserError.reason` string and optional outcome | Tagged reason and required outcome. Use `Effect.catchReason`; factual limit/status/retry fields belong to their reason |
|
|
167
|
+
| All host reason names reach model output | A compact eleven-reason tool failure; original structured fields and bounded call IDs remain in `ToolHost.toolFailures` |
|
|
168
|
+
| `closeChecked` success is `undefined` | Concrete receipt success after the existing ownership check; `close` and `cleanupResult` remain available |
|
|
169
|
+
| Capture `dropped` | `discarded = overflow + late + duplicates + rejected`; no change to default capacity or unknown upstream loss |
|
|
170
|
+
| Non-supervising helper takes default `BrowserSession` | Use `AnySession`; keep supervising helpers generic in callback error or concrete session |
|
|
171
|
+
|
|
172
|
+
Binding omission defaults are one concurrent call, 64 KiB each direction, 10 seconds and
|
|
173
|
+
`reject-call`; explicit values remain validated. Combined readiness uses the most conservative
|
|
174
|
+
`existingDocuments` policy. Common navigation now accepts optional `timeoutMillis` with the same
|
|
175
|
+
bounds as `startNavigation`; direct, retained and pinned operations share it. Observations add
|
|
176
|
+
bounded checked/selected/inputType/required state without field values or destinations. These
|
|
177
|
+
contracts live in the [common browser guide](../browser/README.md).
|
|
178
|
+
|
|
179
|
+
Explicit generic applications of curried `Browser.scoped` use four outer parameters and three
|
|
180
|
+
returned parameters; ordinary inferred syntax is unchanged. Provider resource errors such as
|
|
181
|
+
`SessionError` keep their independent reason schemas. When a control-plane read becomes a browser
|
|
182
|
+
operation failure, the bridge preserves available facts; a resource limit with no measured bound
|
|
183
|
+
becomes `Provider` rather than fabricating `Limit` values. A native connection whose setup fails
|
|
184
|
+
after connection work began carries `unknown` outcome.
|
|
333
185
|
|
|
334
186
|
`test/native/handoff-process.test.ts` exercises that path with a real second process: it starts a fresh runtime that receives only the reference, a target id and fixture addresses, attaches, changes the page and closes as a borrower, and the allocating process then drives the same page and releases it. That is local CDP evidence, not a hosted handoff.
|
|
335
187
|
|
|
336
188
|
## The native engine
|
|
337
189
|
|
|
338
|
-
`browser-binding` names the engine an owned or borrowed browser connects through. The default is Playwright over CDP with the provider's own address, so nothing has to be provided; `BrowserBinding.layer(binding)` supplies another to every browser Layer built beneath it. A binding is opaque and
|
|
190
|
+
`browser-binding` names the engine an owned or borrowed browser connects through. The default is Playwright over CDP with the provider's own address, so nothing has to be provided; `BrowserBinding.layer(binding)` supplies another to every browser Layer built beneath it. A binding is opaque and issued by the common runtime, so a value that merely has its shape is refused before anything is allocated.
|
|
339
191
|
|
|
340
192
|
`BrowserBinding.playwright({ resolveEndpoint, onConnected })` is trusted host configuration for running the unmodified Playwright engine somewhere else, such as a local Chromium. `resolveEndpoint` runs only after the provider-issued address passed the default checks, and chooses the CDP endpoint actually connected to; `onConnected` sees the engine's own browser object before the owner drives it. Neither is model-facing, and a local engine reached this way says nothing about how a hosted connection behaves. This package's own native fixtures use exactly this, rather than replacing `chromium.connectOverCDP` globally.
|
|
341
193
|
|
|
@@ -391,12 +243,9 @@ An agent run or Function invocation that persists a Context writes it from Brows
|
|
|
391
243
|
- `sessions`, `contexts`, `context-coordination` — passive inspection, explicit release, context resources and writer settlement.
|
|
392
244
|
- `extensions` — provisioning a Chrome extension archive once as a durable project resource, and selecting it by reference at launch.
|
|
393
245
|
- `uploads` — placing a file where the running session can already reach it, and the receipt that authorizes attaching it.
|
|
394
|
-
- `
|
|
395
|
-
- `launch`, `references`, `browser-data`, `session-data`, `cleanup`, `transfers`, `errors` — credential-free schemas and typed expected errors.
|
|
246
|
+
- `launch`, `references`, `session-data`, `cleanup`, `transfers`, `errors` — credential-free schemas and typed expected errors.
|
|
396
247
|
- `browser-binding` — the trusted, opaque native engine a browser connects through: Playwright by default, or Playwright routed to a host-resolved endpoint.
|
|
397
248
|
- `browser` — scoped allocation, borrowed attachment to a running session, deterministic page control, real pointer, wheel and key input, host-only tabs/frames/viewport, modeled file selection, Live View handoff, keep-alive detach and explicit reconnect.
|
|
398
|
-
- `capture` — optional target-pinned live-page JPEG frame streams using Playwright 1.63's maintained screencast API. The caller owns encoding, storage and presentation.
|
|
399
|
-
- `page-control` — opt-in host-owned stage holds and explicit receipt-based resume, independent of scout selection.
|
|
400
249
|
- `recordings` — post-session Browserbase MP4 assembly, status and bounded retrieval. Stable identity is session + recording page; signed URLs are refreshed and are not durable identity.
|
|
401
250
|
- `replays` — host-authorized replay metadata and validated HLS media proxy material. It is playback access to a recording, not another recording.
|
|
402
251
|
- `downloads` — ordinary website download metadata, provider-side filters, bounded byte streams and deletion, separate from provider recordings.
|
|
@@ -405,7 +254,7 @@ An agent run or Function invocation that persists a Context writes it from Brows
|
|
|
405
254
|
|
|
406
255
|
Every expected failure says three things. `operation` is what you asked for, from a closed vocabulary per error class: `BrowserError` names the browser operations, `SessionError` only session calls, and so on, so you can match on them exhaustively and a misspelling is a type error, not a string that happens to compile. `reason` is why it failed. `outcome`, when present, is whether the work was sent: `undispatched` is safe to retry, `rejected` was refused, and `unknown` means a mutation may have happened and is never replayed for you. Why an extension archive was refused is a `reason` (`limit`, `unsafe-filename`, `configuration`) of the one `extension-archive` operation. A native step's own name never appears: the driver raises a private failure, and the owner stamps the operation it admitted.
|
|
407
256
|
|
|
408
|
-
Everything under `src/internal/` is private,
|
|
257
|
+
Everything under `src/internal/` is private; the common runtime offers a modeled integration constructor, while no consumer CDP seam is exported; `browser-binding` chooses the engine and where it connects, never what runs over the connection. The driver does hold a CDP session; exposing it, or the ownership internals, would place actions outside the mutation permit that serializes them and outside the fencing that makes an uncertain outcome detectable. Opening a second debugger connection beside this one has the same effect and is equally unsupported. An unmodeled need is a request for a modeled entry point, not a reason to reach around the boundary.
|
|
409
258
|
|
|
410
259
|
## Artifact and capture guarantees
|
|
411
260
|
|
|
@@ -415,101 +264,8 @@ Website downloads retain their provider download ID, safe filename, MIME type an
|
|
|
415
264
|
|
|
416
265
|
Replay playlists reject arbitrary URI-bearing tags and proxy only indexed media from the validated playlist. API credentials are never returned to a browser client.
|
|
417
266
|
|
|
418
|
-
Live
|
|
419
|
-
|
|
420
|
-
`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`, which is part of `dropped`. 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 reason `timestamp`.
|
|
421
|
-
|
|
422
|
-
Closing, navigating, detaching a relevant frame, or resizing the captured page ends its interval explicitly without ending a sibling page's capture. `Capture.start(session, { lifetime: "page" })` instead follows a page's main frame across documents: start it before a navigation and it covers the loading in between. 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 navigation), and the summary's bounded `documentBoundaries` give the last sequence before each one and the address it committed, 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. A definitively closed page releases its capture reservation. 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 `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 `examples/realistic-footage`.
|
|
423
|
-
|
|
424
|
-
### Read metadata while capture is running
|
|
425
|
-
|
|
426
|
-
```ts
|
|
427
|
-
const interval = yield * Capture.start(session, { lifetime: "page" });
|
|
428
|
-
const current = yield * interval.snapshot;
|
|
429
|
-
// current.phase is "capturing"; initialUrl and recorded documentBoundaries are available now.
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
`snapshot` copies the bounded metadata already recorded in host memory. It does
|
|
433
|
-
no browser work, consumes no frames, charges no action, and works while the page
|
|
434
|
-
is held. Repeated reads neither stop nor restart capture. `observedMonotonicNanos`
|
|
435
|
-
stamps the read; each boundary retains its own commit observation time. The
|
|
436
|
-
prefix holds at most 64 boundaries; `documentBoundariesTruncated` reports overflow
|
|
437
|
-
and `currentDocument` continues counting. Addresses longer than the existing
|
|
438
|
-
bound remain `null`. The model should not receive these host-only addresses by
|
|
439
|
-
accident merely because it can inspect the page.
|
|
440
|
-
|
|
441
|
-
While capturing, `reason` and `nativeStop` are `null`. `phase: "stopping"` means
|
|
442
|
-
capture has stopped accepting frames but native cleanup is pending. `"stopped"`
|
|
443
|
-
means that cleanup attempt settled; only `nativeStop: "confirmed"` confirms the
|
|
444
|
-
native stop. All snapshots are observations of accounting so far: buffered frames
|
|
445
|
-
can still be drained after stop. For final accounting, stop or await capture
|
|
446
|
-
termination, finish draining `frames`, then read `completed` to obtain the terminal
|
|
447
|
-
summary with final delivery counts. Preserve the terminal error and loss counters
|
|
448
|
-
when reconciling earlier snapshots. `late` remains included in `dropped`, and
|
|
449
|
-
`upstreamDrops` remains `"unknown"`; live metadata adds no stronger pixel or loss
|
|
450
|
-
guarantee.
|
|
451
|
-
|
|
452
|
-
### What a compositor is given, and what it owns
|
|
453
|
-
|
|
454
|
-
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:
|
|
455
|
-
|
|
456
|
-
- each frame's `receivedMonotonicNanos`, and the `document` it was received during;
|
|
457
|
-
- an `InputReceipt` for every native pointer and key operation, with the position commanded and an interval on that same clock;
|
|
458
|
-
- 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 boundary, read inside the navigation event that committed it. 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.
|
|
459
|
-
|
|
460
|
-
A boundary is the commit, and it is the only moment of a navigation an application cannot see for itself. When the navigation started and when its document finished loading are yours to stamp around the operation that caused it, because you made the call: `startNavigation` returns at dispatch and its `completed` resolves at DOMContentLoaded. The clock is Effect's `Clock`, read where the session was opened and the capture was started, so `Clock.clockWith((clock) => clock.monotonicTimeNanos)` in the same runtime is on the same timeline as every frame and receipt. A title is page state that changes whenever the page likes, not part of a transition: read it with `session.pages` when you need one, and stamp that read yourself.
|
|
461
|
-
|
|
462
|
-
The two clocks are never related for you. `sourceTimeMillis` is the browser's wall clock; everything above is the host's monotonic clock; on a hosted session they differ by an offset this package cannot observe. Every frame it receives is already late by the very capture latency it would be trying to measure, and nothing else it is handed carries the browser's time. Relating them takes a round trip into the page, which costs either a charged action or a registered binding, on a schedule only the application can choose, and it fails on a held page. So place input on frames by receipt time, which is always available and late by the capture latency, or measure the offset yourself with a four-timestamp exchange over a typed binding, as NTP does: the page stamps when it called and when the reply arrived, the host stamps when it received and when it replied, and half the best round trip is the error bound to report beside the offset.
|
|
463
|
-
|
|
464
|
-
## Explicit stage-page holds (opt-in)
|
|
465
|
-
|
|
466
|
-
Set `pageControl: true` on `BrowserbaseBrowser.layer` to use the host-only `page-control` module. The default remains off. `PageControl.suspend(session, page)` returns a live `PageSuspension`; `PageControl.resume(session, receipt)` consumes that exact receipt. Use a `PageInfo` from `session.pages`. 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 `session.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.
|
|
467
|
-
|
|
468
|
-
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 provider allocation; 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.
|
|
469
|
-
|
|
470
|
-
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.
|
|
471
|
-
|
|
472
|
-
Partial native failures fence the session as uncertain; 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.
|
|
267
|
+
Live frames and page holds follow the common [capture guarantees](../browser/README.md#live-capture-and-presentation). The provider's recording/replay services retain their independent lifetime after the browser scope ends. A downstream encoder consumes public frame streams; it does not own another browser.
|
|
473
268
|
|
|
474
269
|
## Development and evidence
|
|
475
270
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
`tools/packed-consumer.sh` installs the emitted npm tarballs into separate consumers with exact public dependency versions, checks NodeNext declarations, and runs the unchanged native suites without workspace aliases. It also runs real programs directly on Node and Bun: `test/consumer/resources.ts` with no Playwright and no framework installed, and `test/consumer/native.ts` against a real local Chromium.
|
|
479
|
-
|
|
480
|
-
## Independent local Chromium
|
|
481
|
-
|
|
482
|
-
`effect-browserbase/local-browser` provides `LocalBrowser`. 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.
|
|
483
|
-
|
|
484
|
-
```ts
|
|
485
|
-
import { Effect } from "effect";
|
|
486
|
-
import { BrowserPolicy, NavigateRequest } from "effect-browserbase/browser-data";
|
|
487
|
-
import { LocalBrowser } from "effect-browserbase/local-browser";
|
|
488
|
-
|
|
489
|
-
const program = Effect.scoped(
|
|
490
|
-
Effect.gen(function* () {
|
|
491
|
-
const browser = yield* LocalBrowser;
|
|
492
|
-
const session = yield* browser.open(BrowserPolicy.unrestricted());
|
|
493
|
-
yield* session.bind().navigate(NavigateRequest.make({ url: "https://example.com" }));
|
|
494
|
-
return yield* session.observe({ scope: "viewport" });
|
|
495
|
-
}),
|
|
496
|
-
).pipe(
|
|
497
|
-
Effect.provide(
|
|
498
|
-
LocalBrowser.layer({
|
|
499
|
-
viewport: { width: 1280, height: 720 },
|
|
500
|
-
pageControl: true,
|
|
501
|
-
launch: { headless: true, chromiumSandbox: true },
|
|
502
|
-
}),
|
|
503
|
-
),
|
|
504
|
-
);
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
Layer construction validates configuration and starts nothing. The optional `playwright-core` peer is loaded only when needed. `acquire(policy, { bootstrap })` starts one owned Chromium process and registers cleanup before waiting for a connection; its cached `connect` yields one `LocalSession<E>`. `open` combines those steps. Bootstrap consumer errors and services remain in the acquisition signatures, just as for hosted sessions. `withBrowser(policy, request, use)` supervises callback failure and checks local cleanup before returning a normal result.
|
|
508
|
-
|
|
509
|
-
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.
|
|
510
|
-
|
|
511
|
-
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.
|
|
512
|
-
|
|
513
|
-
Local identity is `{ provider: "local", 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.
|
|
514
|
-
|
|
515
|
-
`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. Hosted endpoint validation and network policy are unchanged.
|
|
271
|
+
Read [Contributing](../../CONTRIBUTING.md) for the frozen compatibility workspace and required checks. Provider unit tests cover HTTP/resources and release reconciliation. Cross-package source regression tests live in `test/integration`. The `generic` and `agent-hosted` installed consumers run the maintained native suites against real Chromium with scripted provider HTTP; the `resources` consumer installs no Playwright or Effect Agent. Hosted evidence remains separately recorded in [Status](../../docs/STATUS.md) and never inferred from a local pass.
|
package/dist/Account.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ClientError } from "./Errors.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { BrowserbaseClient, ClientOptions } from "./Client.mjs";
|
|
3
3
|
import { BrowserbaseAgents } from "./Agents.mjs";
|
|
4
4
|
import { BrowserbaseCertificates } from "./Certificates.mjs";
|
|
5
5
|
import { BrowserbaseContexts } from "./Contexts.mjs";
|
|
6
|
-
import { t as BrowserbaseSessions } from "./Sessions-
|
|
6
|
+
import { t as BrowserbaseSessions } from "./Sessions-tFJYaXFq.mjs";
|
|
7
7
|
import { BrowserbaseDownloads } from "./Downloads.mjs";
|
|
8
8
|
import { BrowserbaseExtensions } from "./Extensions.mjs";
|
|
9
9
|
import { BrowserbaseFunctions } from "./Functions.mjs";
|
|
@@ -15,9 +15,7 @@ import { BrowserbaseSearch } from "./Search.mjs";
|
|
|
15
15
|
import { BrowserbaseUploads } from "./Uploads.mjs";
|
|
16
16
|
import { BrowserbaseWebhooks } from "./Webhooks.mjs";
|
|
17
17
|
import { Config, Layer } from "effect";
|
|
18
|
-
|
|
19
|
-
export { Services, layer, layerConfig };
|
|
20
|
-
}
|
|
18
|
+
//#region src/Account.d.ts
|
|
21
19
|
/** Every resource service this package owns, on one account's authority. */
|
|
22
20
|
export type Services = BrowserbaseAgents | BrowserbaseCertificates | BrowserbaseClient | BrowserbaseContexts | BrowserbaseDownloads | BrowserbaseExtensions | BrowserbaseFunctions | BrowserbasePageFetch | BrowserbaseProjects | BrowserbaseRecordings | BrowserbaseReplays | BrowserbaseSearch | BrowserbaseSessions | BrowserbaseUploads | BrowserbaseWebhooks;
|
|
23
21
|
/**
|
|
@@ -30,5 +28,5 @@ export declare const layer: (options: ClientOptions) => Layer.Layer<Services, Cl
|
|
|
30
28
|
/** The same bundle, taking account authority from the application's `ConfigProvider`. */
|
|
31
29
|
export declare const layerConfig: (options?: Omit<ClientOptions, "projectId" | "apiKey">) => Layer.Layer<Services, ClientError | Config.ConfigError>;
|
|
32
30
|
//#endregion
|
|
33
|
-
export {
|
|
31
|
+
export {};
|
|
34
32
|
//# sourceMappingURL=Account.d.mts.map
|
package/dist/Account.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { t as BrowserbaseClient } from "./Client-D1_xYM9o.mjs";
|
|
1
|
+
import { t as BrowserbaseClient } from "./Client-BQteUePT.mjs";
|
|
3
2
|
import { BrowserbaseAgents } from "./Agents.mjs";
|
|
4
3
|
import { BrowserbaseCertificates } from "./Certificates.mjs";
|
|
5
4
|
import { BrowserbaseContexts } from "./Contexts.mjs";
|
|
6
|
-
import { t as BrowserbaseSessions } from "./Sessions-
|
|
5
|
+
import { t as BrowserbaseSessions } from "./Sessions-DoQGg9nB.mjs";
|
|
7
6
|
import { BrowserbaseDownloads } from "./Downloads.mjs";
|
|
8
|
-
import { t as BrowserbaseExtensions } from "./Extensions-
|
|
7
|
+
import { t as BrowserbaseExtensions } from "./Extensions-nMNMPGCc.mjs";
|
|
9
8
|
import { BrowserbaseFunctions } from "./Functions.mjs";
|
|
10
9
|
import { BrowserbasePageFetch } from "./PageFetch.mjs";
|
|
11
10
|
import { BrowserbaseProjects } from "./Projects.mjs";
|
|
@@ -16,10 +15,6 @@ import { BrowserbaseUploads } from "./Uploads.mjs";
|
|
|
16
15
|
import { BrowserbaseWebhooks } from "./Webhooks.mjs";
|
|
17
16
|
import { Layer } from "effect";
|
|
18
17
|
//#region src/Account.ts
|
|
19
|
-
var Account_exports = /* @__PURE__ */ __exportAll({
|
|
20
|
-
layer: () => layer,
|
|
21
|
-
layerConfig: () => layerConfig
|
|
22
|
-
});
|
|
23
18
|
/**
|
|
24
19
|
* The services that read one Client. Sessions is provided first and kept in the output,
|
|
25
20
|
* because the artifact services read it and a caller composing this Layer should not have
|
|
@@ -36,6 +31,6 @@ const layer = (options) => services.pipe(Layer.provideMerge(BrowserbaseClient.la
|
|
|
36
31
|
/** The same bundle, taking account authority from the application's `ConfigProvider`. */
|
|
37
32
|
const layerConfig = (options = {}) => services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));
|
|
38
33
|
//#endregion
|
|
39
|
-
export { layer, layerConfig
|
|
34
|
+
export { layer, layerConfig };
|
|
40
35
|
|
|
41
36
|
//# sourceMappingURL=Account.mjs.map
|
package/dist/Account.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Account.mjs","names":[],"sources":["../src/Account.ts"],"sourcesContent":["import { type Config, Layer } from \"effect\";\n\nimport { BrowserbaseAgents } from \"./Agents.ts\";\nimport { BrowserbaseCertificates } from \"./Certificates.ts\";\nimport { BrowserbaseClient, type ClientOptions } from \"./Client.ts\";\nimport { BrowserbaseContexts } from \"./Contexts.ts\";\nimport { BrowserbaseDownloads } from \"./Downloads.ts\";\nimport type { ClientError } from \"./Errors.ts\";\nimport { BrowserbaseExtensions } from \"./Extensions.ts\";\nimport { BrowserbaseFunctions } from \"./Functions.ts\";\nimport { BrowserbasePageFetch } from \"./PageFetch.ts\";\nimport { BrowserbaseProjects } from \"./Projects.ts\";\nimport { BrowserbaseRecordings } from \"./Recordings.ts\";\nimport { BrowserbaseReplays } from \"./Replays.ts\";\nimport { BrowserbaseSearch } from \"./Search.ts\";\nimport { BrowserbaseSessions } from \"./Sessions.ts\";\nimport { BrowserbaseUploads } from \"./Uploads.ts\";\nimport { BrowserbaseWebhooks } from \"./Webhooks.ts\";\n\n/** Every resource service this package owns, on one account's authority. */\nexport type Services =\n | BrowserbaseAgents\n | BrowserbaseCertificates\n | BrowserbaseClient\n | BrowserbaseContexts\n | BrowserbaseDownloads\n | BrowserbaseExtensions\n | BrowserbaseFunctions\n | BrowserbasePageFetch\n | BrowserbaseProjects\n | BrowserbaseRecordings\n | BrowserbaseReplays\n | BrowserbaseSearch\n | BrowserbaseSessions\n | BrowserbaseUploads\n | BrowserbaseWebhooks;\n\n/**\n * The services that read one Client. Sessions is provided first and kept in the output,\n * because the artifact services read it and a caller composing this Layer should not have\n * to know which ones do.\n */\nconst services = Layer.mergeAll(\n BrowserbaseAgents.layer,\n BrowserbaseCertificates.layer,\n BrowserbaseContexts.layer,\n BrowserbaseDownloads.layer,\n BrowserbaseExtensions.layer,\n BrowserbaseFunctions.layer,\n BrowserbasePageFetch.layer,\n BrowserbaseProjects.layer,\n BrowserbaseRecordings.layer,\n BrowserbaseReplays.layer,\n BrowserbaseSearch.layer,\n BrowserbaseUploads.layer,\n BrowserbaseWebhooks.layer,\n).pipe(Layer.provideMerge(BrowserbaseSessions.layer));\n\n/**\n * One account for every resource operation, so credentials are composed once rather than\n * per service. Building it performs no I/O, allocates nothing and loads no native peer;\n * `BrowserbaseBrowser.layer` is deliberately separate, because a browser fixes budgets and\n * a connection lifetime that an account does not.\n */\nexport const layer = (options: ClientOptions): Layer.Layer<Services, ClientError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layer(options)));\n\n/** The same bundle, taking account authority from the application's `ConfigProvider`. */\nexport const layerConfig = (\n options: Omit<ClientOptions, \"projectId\" | \"apiKey\"> = {},\n): Layer.Layer<Services, ClientError | Config.ConfigError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"Account.mjs","names":[],"sources":["../src/Account.ts"],"sourcesContent":["import { type Config, Layer } from \"effect\";\n\nimport { BrowserbaseAgents } from \"./Agents.ts\";\nimport { BrowserbaseCertificates } from \"./Certificates.ts\";\nimport { BrowserbaseClient, type ClientOptions } from \"./Client.ts\";\nimport { BrowserbaseContexts } from \"./Contexts.ts\";\nimport { BrowserbaseDownloads } from \"./Downloads.ts\";\nimport type { ClientError } from \"./Errors.ts\";\nimport { BrowserbaseExtensions } from \"./Extensions.ts\";\nimport { BrowserbaseFunctions } from \"./Functions.ts\";\nimport { BrowserbasePageFetch } from \"./PageFetch.ts\";\nimport { BrowserbaseProjects } from \"./Projects.ts\";\nimport { BrowserbaseRecordings } from \"./Recordings.ts\";\nimport { BrowserbaseReplays } from \"./Replays.ts\";\nimport { BrowserbaseSearch } from \"./Search.ts\";\nimport { BrowserbaseSessions } from \"./Sessions.ts\";\nimport { BrowserbaseUploads } from \"./Uploads.ts\";\nimport { BrowserbaseWebhooks } from \"./Webhooks.ts\";\n\n/** Every resource service this package owns, on one account's authority. */\nexport type Services =\n | BrowserbaseAgents\n | BrowserbaseCertificates\n | BrowserbaseClient\n | BrowserbaseContexts\n | BrowserbaseDownloads\n | BrowserbaseExtensions\n | BrowserbaseFunctions\n | BrowserbasePageFetch\n | BrowserbaseProjects\n | BrowserbaseRecordings\n | BrowserbaseReplays\n | BrowserbaseSearch\n | BrowserbaseSessions\n | BrowserbaseUploads\n | BrowserbaseWebhooks;\n\n/**\n * The services that read one Client. Sessions is provided first and kept in the output,\n * because the artifact services read it and a caller composing this Layer should not have\n * to know which ones do.\n */\nconst services = Layer.mergeAll(\n BrowserbaseAgents.layer,\n BrowserbaseCertificates.layer,\n BrowserbaseContexts.layer,\n BrowserbaseDownloads.layer,\n BrowserbaseExtensions.layer,\n BrowserbaseFunctions.layer,\n BrowserbasePageFetch.layer,\n BrowserbaseProjects.layer,\n BrowserbaseRecordings.layer,\n BrowserbaseReplays.layer,\n BrowserbaseSearch.layer,\n BrowserbaseUploads.layer,\n BrowserbaseWebhooks.layer,\n).pipe(Layer.provideMerge(BrowserbaseSessions.layer));\n\n/**\n * One account for every resource operation, so credentials are composed once rather than\n * per service. Building it performs no I/O, allocates nothing and loads no native peer;\n * `BrowserbaseBrowser.layer` is deliberately separate, because a browser fixes budgets and\n * a connection lifetime that an account does not.\n */\nexport const layer = (options: ClientOptions): Layer.Layer<Services, ClientError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layer(options)));\n\n/** The same bundle, taking account authority from the application's `ConfigProvider`. */\nexport const layerConfig = (\n options: Omit<ClientOptions, \"projectId\" | \"apiKey\"> = {},\n): Layer.Layer<Services, ClientError | Config.ConfigError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,WAAW,MAAM,SACrB,kBAAkB,OAClB,wBAAwB,OACxB,oBAAoB,OACpB,qBAAqB,OACrB,sBAAsB,OACtB,qBAAqB,OACrB,qBAAqB,OACrB,oBAAoB,OACpB,sBAAsB,OACtB,mBAAmB,OACnB,kBAAkB,OAClB,mBAAmB,OACnB,oBAAoB,KACtB,CAAC,CAAC,KAAK,MAAM,aAAa,oBAAoB,KAAK,CAAC;;;;;;;AAQpD,MAAa,SAAS,YACpB,SAAS,KAAK,MAAM,aAAa,kBAAkB,MAAM,OAAO,CAAC,CAAC;;AAGpE,MAAa,eACX,UAAuD,CAAC,MAExD,SAAS,KAAK,MAAM,aAAa,kBAAkB,YAAY,OAAO,CAAC,CAAC"}
|