stagehand 4.3.1a0.dev1658__tar.gz → 4.4.0a0.dev1661__tar.gz
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.
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/PKG-INFO +1 -1
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/pyproject.toml +1 -1
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/manifest.json +2 -2
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/service-worker.js +251 -44
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_generated/protocol_version.py +1 -1
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browser.py +191 -18
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/cdp_client.py +100 -2
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/stagehand.py +1 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/README.md +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/__init__.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/blank.html +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/content-script.js +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/wake-service-worker.html +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/wake-service-worker.js +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_generated/__init__.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_generated/input_types.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_generated/models.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_sdk_identity.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_validation.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browser_clipboard.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browser_context.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_connection.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_rpc.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_services.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_session.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_transport.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/client_models.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/client_types.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/extension_assets.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/file_upload.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/locator.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/page.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/py.typed +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/response.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/rpc_client.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/timeouts.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/webmcp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stagehand
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0a0.dev1661
|
|
4
4
|
Summary: Browserbase's SDK for building browser agents
|
|
5
5
|
Keywords: ai,agent,ai-agent,browser,browser-agent,browser-automation,computer-use,headless,playwright,cdp,llm,automation,web-scraping,testing
|
|
6
6
|
Author: Browserbase
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/manifest.json
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Stagehand Runtime",
|
|
4
|
-
"minimum_chrome_version": "
|
|
4
|
+
"minimum_chrome_version": "137",
|
|
5
5
|
"permissions": [
|
|
6
6
|
"debugger",
|
|
7
7
|
"offscreen",
|
|
@@ -31,5 +31,5 @@
|
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
33
|
"options_page": "wake-service-worker.html",
|
|
34
|
-
"version": "1.
|
|
34
|
+
"version": "1.3.0"
|
|
35
35
|
}
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/service-worker.js
RENAMED
|
@@ -9206,7 +9206,7 @@ function isRecord$1(value) {
|
|
|
9206
9206
|
}
|
|
9207
9207
|
var package_default$1 = {
|
|
9208
9208
|
name: "@browserbasehq/stagehand-protocol",
|
|
9209
|
-
version: "3.
|
|
9209
|
+
version: "3.1.1-alpha-44d99dfbdd24baaf06986492792348582266d8f0",
|
|
9210
9210
|
"private": true,
|
|
9211
9211
|
type: "module",
|
|
9212
9212
|
exports: {
|
|
@@ -13213,6 +13213,7 @@ var RPCClient = class {
|
|
|
13213
13213
|
this.nextRequestId = 1;
|
|
13214
13214
|
this.pending = /* @__PURE__ */ new Map();
|
|
13215
13215
|
this.closed = false;
|
|
13216
|
+
this.clientConnected = true;
|
|
13216
13217
|
this.runtime.onmessage = (message, runtimeAttachments) => this.receive(message, runtimeAttachments);
|
|
13217
13218
|
this.runtime.onclose = (reason) => this.close(reason);
|
|
13218
13219
|
this.runtime.onerror = (error) => this.close(error);
|
|
@@ -13220,6 +13221,7 @@ var RPCClient = class {
|
|
|
13220
13221
|
async send(method, params, options = {}) {
|
|
13221
13222
|
options.signal?.throwIfAborted();
|
|
13222
13223
|
if (this.closed) throw new Error("RPC client is closed");
|
|
13224
|
+
if (!this.clientConnected) throw new Error("Stagehand client disconnected");
|
|
13223
13225
|
const parentContext = context.active();
|
|
13224
13226
|
const span = this.router.runtime.tracing.tracer.startSpan(method.name, {
|
|
13225
13227
|
kind: SpanKind.CLIENT,
|
|
@@ -13293,6 +13295,14 @@ var RPCClient = class {
|
|
|
13293
13295
|
this.runtime.onerror = void 0;
|
|
13294
13296
|
this.runtime.close();
|
|
13295
13297
|
}
|
|
13298
|
+
/** Fail reverse RPCs whose client disappeared, allowing instance disposal to drain. */
|
|
13299
|
+
releaseClient() {
|
|
13300
|
+
this.clientConnected = false;
|
|
13301
|
+
for (const [id] of this.pending) this.rejectPending(id, /* @__PURE__ */ new Error("Stagehand client disconnected"));
|
|
13302
|
+
}
|
|
13303
|
+
acceptClient() {
|
|
13304
|
+
this.clientConnected = true;
|
|
13305
|
+
}
|
|
13296
13306
|
waitForResponse(id, method, cleanup = () => {}) {
|
|
13297
13307
|
return new Promise((resolve, reject) => {
|
|
13298
13308
|
this.pending.set(id, {
|
|
@@ -56384,7 +56394,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
|
|
|
56384
56394
|
};
|
|
56385
56395
|
var package_default = {
|
|
56386
56396
|
name: "@browserbasehq/stagehand-extension",
|
|
56387
|
-
version: "1.
|
|
56397
|
+
version: "1.3.0-alpha-44d99dfbdd24baaf06986492792348582266d8f0",
|
|
56388
56398
|
"private": true,
|
|
56389
56399
|
type: "module",
|
|
56390
56400
|
scripts: {
|
|
@@ -56591,7 +56601,7 @@ var FrameRegistry = class {
|
|
|
56591
56601
|
* Record that a frame attached. If `parentId` is null and `frameId` differs from the current
|
|
56592
56602
|
* root, this is a root swap and we rename the root id.
|
|
56593
56603
|
*
|
|
56594
|
-
*
|
|
56604
|
+
* The provided `sessionId` is the selected **owner** for the new/attached frame.
|
|
56595
56605
|
*/
|
|
56596
56606
|
onFrameAttached(frameId, parentId, sessionId) {
|
|
56597
56607
|
if (!parentId && frameId !== this.rootFrameId) {
|
|
@@ -58439,7 +58449,7 @@ var Page = class Page {
|
|
|
58439
58449
|
}
|
|
58440
58450
|
/**
|
|
58441
58451
|
* Parent/child session emitted a `frameAttached`.
|
|
58442
|
-
* Topology update + ownership stamped to
|
|
58452
|
+
* Topology update + ownership stamped to the session selected by Context.
|
|
58443
58453
|
*/
|
|
58444
58454
|
onFrameAttached(frameId, parentId, session) {
|
|
58445
58455
|
this.ensureOrdinal(frameId);
|
|
@@ -58487,7 +58497,7 @@ var Page = class Page {
|
|
|
58487
58497
|
* An OOPIF child session whose **main** frame id equals the parent iframe’s frameId
|
|
58488
58498
|
* has been attached; adopt the session into this Page and seed ownership for its subtree.
|
|
58489
58499
|
*/
|
|
58490
|
-
adoptOopifSession(childSession, childMainFrameId) {
|
|
58500
|
+
adoptOopifSession(childSession, childMainFrameId, onFrameTreeSeeded) {
|
|
58491
58501
|
if (this.disposed) return;
|
|
58492
58502
|
const previous = childSession.id ? this.sessions.get(childSession.id) : void 0;
|
|
58493
58503
|
if (childSession.id) this.sessions.set(childSession.id, childSession);
|
|
@@ -58502,9 +58512,6 @@ var Page = class Page {
|
|
|
58502
58512
|
childSession.on("Page.frameNavigated", (evt) => {
|
|
58503
58513
|
this.onFrameNavigated(evt.frame, childSession);
|
|
58504
58514
|
});
|
|
58505
|
-
childSession.on("Page.frameAttached", (evt) => {
|
|
58506
|
-
this.onFrameAttached(evt.frameId, evt.parentFrameId ?? null, childSession);
|
|
58507
|
-
});
|
|
58508
58515
|
childSession.on("Page.frameDetached", (evt) => {
|
|
58509
58516
|
this.onFrameDetached(evt.frameId, evt.reason ?? "remove");
|
|
58510
58517
|
});
|
|
@@ -58522,6 +58529,7 @@ var Page = class Page {
|
|
|
58522
58529
|
if (!this.disposed && this.sessions.get(childSession.id ?? "") === childSession) {
|
|
58523
58530
|
this.registry.seedFromFrameTree(childSession.id ?? "child", frameTree);
|
|
58524
58531
|
this.reconcilePendingWebMCPTools();
|
|
58532
|
+
onFrameTreeSeeded?.();
|
|
58525
58533
|
}
|
|
58526
58534
|
} catch {}
|
|
58527
58535
|
})();
|
|
@@ -60379,6 +60387,7 @@ var StagehandRuntime = class {
|
|
|
60379
60387
|
this.state = createStore()(() => StagehandRuntimeStateSchema.parse({ status: "idle" }));
|
|
60380
60388
|
this.pagesById = /* @__PURE__ */ new Map();
|
|
60381
60389
|
this.pageEventSubscriptions = /* @__PURE__ */ new Map();
|
|
60390
|
+
this.browserConnectionGeneration = 0;
|
|
60382
60391
|
this.initializationInProgress = false;
|
|
60383
60392
|
this.lifecycleTail = Promise.resolve();
|
|
60384
60393
|
this.stagehandInstanceClosing = false;
|
|
@@ -60387,6 +60396,7 @@ var StagehandRuntime = class {
|
|
|
60387
60396
|
}
|
|
60388
60397
|
async replaceBrowserConnection(params, bootstrapLogger) {
|
|
60389
60398
|
const { cdpUrl } = params;
|
|
60399
|
+
const generation = ++this.browserConnectionGeneration;
|
|
60390
60400
|
const previousSession = this.browserSession;
|
|
60391
60401
|
this.browserSession = void 0;
|
|
60392
60402
|
this.browserSessionCdpUrl = void 0;
|
|
@@ -60395,14 +60405,35 @@ var StagehandRuntime = class {
|
|
|
60395
60405
|
this.responseHandles.clear();
|
|
60396
60406
|
await previousSession?.close();
|
|
60397
60407
|
try {
|
|
60398
|
-
|
|
60408
|
+
const session = await this.adapters.browserSessionFactory(cdpUrl, this.logger, bootstrapLogger);
|
|
60409
|
+
if (generation !== this.browserConnectionGeneration) {
|
|
60410
|
+
await session.close();
|
|
60411
|
+
throw new Error("Stagehand browser connection was dropped while connecting");
|
|
60412
|
+
}
|
|
60413
|
+
this.browserSession = session;
|
|
60399
60414
|
this.browserSessionCdpUrl = cdpUrl;
|
|
60400
60415
|
} catch (error) {
|
|
60401
|
-
|
|
60402
|
-
|
|
60416
|
+
if (generation === this.browserConnectionGeneration) {
|
|
60417
|
+
await this.browserSession?.close();
|
|
60418
|
+
this.browserSession = void 0;
|
|
60419
|
+
}
|
|
60403
60420
|
throw error;
|
|
60404
60421
|
}
|
|
60405
60422
|
}
|
|
60423
|
+
/**
|
|
60424
|
+
* Closes the runtime's own browser connection immediately, outside the lifecycle
|
|
60425
|
+
* queue: a lost client must not wait behind lifecycle work that needs that client.
|
|
60426
|
+
*/
|
|
60427
|
+
async dropBrowserConnection() {
|
|
60428
|
+
this.browserConnectionGeneration += 1;
|
|
60429
|
+
const session = this.browserSession;
|
|
60430
|
+
this.browserSession = void 0;
|
|
60431
|
+
this.browserSessionCdpUrl = void 0;
|
|
60432
|
+
await session?.close();
|
|
60433
|
+
}
|
|
60434
|
+
async ensureBrowserConnection(cdpUrl) {
|
|
60435
|
+
if (!this.browserSession?.connected || this.browserSessionCdpUrl !== cdpUrl) await this.replaceBrowserConnection({ cdpUrl });
|
|
60436
|
+
}
|
|
60406
60437
|
async initialize(params, logger = this.logger) {
|
|
60407
60438
|
if (this.initializationInProgress) throw new Error("Stagehand initialization is already in progress");
|
|
60408
60439
|
this.initializationInProgress = true;
|
|
@@ -61394,6 +61425,96 @@ function getDomainPolicyDecision(url, policy) {
|
|
|
61394
61425
|
};
|
|
61395
61426
|
return { action: "continue" };
|
|
61396
61427
|
}
|
|
61428
|
+
var RequestSchema = discriminatedUnion("action", [strictObject({
|
|
61429
|
+
action: literal("attach"),
|
|
61430
|
+
targetId: string().min(1),
|
|
61431
|
+
cdpUrl: string().min(1),
|
|
61432
|
+
closeBrowserOnDisconnect: boolean()
|
|
61433
|
+
}), strictObject({
|
|
61434
|
+
action: literal("detach"),
|
|
61435
|
+
targetId: string().min(1)
|
|
61436
|
+
})]);
|
|
61437
|
+
/** Browser-side ownership of one SDK attachment. No heartbeat or expiry timer. */
|
|
61438
|
+
var ClientLifecycle = class {
|
|
61439
|
+
constructor(options) {
|
|
61440
|
+
this.options = options;
|
|
61441
|
+
this.tail = Promise.resolve();
|
|
61442
|
+
}
|
|
61443
|
+
handle(input) {
|
|
61444
|
+
const request = RequestSchema.parse(input);
|
|
61445
|
+
return this.enqueue(async () => {
|
|
61446
|
+
if (request.action === "detach") {
|
|
61447
|
+
const attachment = this.attachment;
|
|
61448
|
+
if (attachment?.targetId !== request.targetId) {
|
|
61449
|
+
if (this.detachedTargetId === request.targetId) return;
|
|
61450
|
+
throw new Error("Stagehand client attachment is no longer active");
|
|
61451
|
+
}
|
|
61452
|
+
attachment.closeBrowserOnDisconnect = false;
|
|
61453
|
+
this.options.rejectClientCallbacks();
|
|
61454
|
+
await this.options.closeConnection();
|
|
61455
|
+
this.removeAttachment();
|
|
61456
|
+
this.detachedTargetId = request.targetId;
|
|
61457
|
+
this.options.disposeClient().catch((error) => this.options.onError(error));
|
|
61458
|
+
return;
|
|
61459
|
+
}
|
|
61460
|
+
if (this.attachment) {
|
|
61461
|
+
const previous = this.attachment;
|
|
61462
|
+
if ((await previous.connection.getTargets()).some((target) => target.targetId === previous.targetId)) throw new Error("A Stagehand client is already connected");
|
|
61463
|
+
await this.lost(previous);
|
|
61464
|
+
}
|
|
61465
|
+
const connection = await this.options.connection(request.cdpUrl);
|
|
61466
|
+
const attachment = {
|
|
61467
|
+
...request,
|
|
61468
|
+
connection,
|
|
61469
|
+
onDestroyed: ({ targetId }) => {
|
|
61470
|
+
if (targetId !== request.targetId) return;
|
|
61471
|
+
this.enqueue(() => this.lost(attachment)).catch((error) => this.options.onError(error));
|
|
61472
|
+
}
|
|
61473
|
+
};
|
|
61474
|
+
this.attachment = attachment;
|
|
61475
|
+
connection.on("Target.targetDestroyed", attachment.onDestroyed);
|
|
61476
|
+
try {
|
|
61477
|
+
const target = (await connection.getTargets()).find((target) => target.targetId === request.targetId);
|
|
61478
|
+
if (!target) {
|
|
61479
|
+
await this.lost(attachment);
|
|
61480
|
+
throw new Error("Stagehand client disconnected during registration");
|
|
61481
|
+
}
|
|
61482
|
+
if (target.url !== "about:blank#stagehand-client-connection") throw new Error("Invalid Stagehand client connection target");
|
|
61483
|
+
} catch (error) {
|
|
61484
|
+
if (this.attachment === attachment) this.removeAttachment();
|
|
61485
|
+
throw error;
|
|
61486
|
+
}
|
|
61487
|
+
});
|
|
61488
|
+
}
|
|
61489
|
+
async lost(attachment) {
|
|
61490
|
+
if (this.attachment !== attachment) return;
|
|
61491
|
+
this.removeAttachment();
|
|
61492
|
+
if (attachment.closeBrowserOnDisconnect) {
|
|
61493
|
+
try {
|
|
61494
|
+
await attachment.connection.send("Browser.close");
|
|
61495
|
+
} catch (error) {
|
|
61496
|
+
if (attachment.connection.connected) throw error;
|
|
61497
|
+
}
|
|
61498
|
+
return;
|
|
61499
|
+
}
|
|
61500
|
+
this.options.rejectClientCallbacks();
|
|
61501
|
+
try {
|
|
61502
|
+
await this.options.closeConnection();
|
|
61503
|
+
} finally {
|
|
61504
|
+
this.options.disposeClient().catch((error) => this.options.onError(error));
|
|
61505
|
+
}
|
|
61506
|
+
}
|
|
61507
|
+
removeAttachment() {
|
|
61508
|
+
const attachment = this.attachment;
|
|
61509
|
+
this.attachment = void 0;
|
|
61510
|
+
attachment?.connection.off("Target.targetDestroyed", attachment.onDestroyed);
|
|
61511
|
+
}
|
|
61512
|
+
enqueue(work) {
|
|
61513
|
+
const result = this.tail.then(work, work);
|
|
61514
|
+
this.tail = result.catch(() => void 0);
|
|
61515
|
+
return result;
|
|
61516
|
+
}
|
|
61517
|
+
};
|
|
61397
61518
|
//#endregion
|
|
61398
61519
|
//#region understudy/context.ts
|
|
61399
61520
|
function isMissingTargetError(error) {
|
|
@@ -61414,12 +61535,13 @@ function hasInjectableDOM(url) {
|
|
|
61414
61535
|
return false;
|
|
61415
61536
|
}
|
|
61416
61537
|
function isNonWebTarget(info) {
|
|
61538
|
+
if (info.url === "about:blank#stagehand-client-connection") return true;
|
|
61417
61539
|
if (info.type === "page") return false;
|
|
61418
61540
|
return info.type !== "iframe" || !hasInjectableDOM(info.url);
|
|
61419
61541
|
}
|
|
61420
61542
|
function isTopLevelPage(info) {
|
|
61421
61543
|
const ti = info;
|
|
61422
|
-
return info.type === "page" && ti.subtype !== "iframe";
|
|
61544
|
+
return info.type === "page" && ti.subtype !== "iframe" && info.url !== "about:blank#stagehand-client-connection";
|
|
61423
61545
|
}
|
|
61424
61546
|
var DEFAULT_ACTIVE_PAGE_TIMEOUT_MS = 3e3;
|
|
61425
61547
|
/**
|
|
@@ -61450,6 +61572,8 @@ var BrowserContext = class BrowserContext {
|
|
|
61450
61572
|
this.sessionOwnerPage = /* @__PURE__ */ new Map();
|
|
61451
61573
|
this.frameOwnerPage = /* @__PURE__ */ new Map();
|
|
61452
61574
|
this.pendingOopifByMainFrame = /* @__PURE__ */ new Map();
|
|
61575
|
+
this.pendingOopifParentByMainFrame = /* @__PURE__ */ new Map();
|
|
61576
|
+
this.adoptedOopifSessionByFrame = /* @__PURE__ */ new Map();
|
|
61453
61577
|
this.createdAtByTarget = /* @__PURE__ */ new Map();
|
|
61454
61578
|
this.typeByTarget = /* @__PURE__ */ new Map();
|
|
61455
61579
|
this.pendingCreatedTargetUrl = /* @__PURE__ */ new Map();
|
|
@@ -61775,6 +61899,8 @@ var BrowserContext = class BrowserContext {
|
|
|
61775
61899
|
this.sessionOwnerPage.clear();
|
|
61776
61900
|
this.frameOwnerPage.clear();
|
|
61777
61901
|
this.pendingOopifByMainFrame.clear();
|
|
61902
|
+
this.pendingOopifParentByMainFrame.clear();
|
|
61903
|
+
this.adoptedOopifSessionByFrame.clear();
|
|
61778
61904
|
this.createdAtByTarget.clear();
|
|
61779
61905
|
this.typeByTarget.clear();
|
|
61780
61906
|
this.pendingCreatedTargetUrl.clear();
|
|
@@ -61979,34 +62105,91 @@ var BrowserContext = class BrowserContext {
|
|
|
61979
62105
|
this.pendingCreatedTargetUrl.delete(info.targetId);
|
|
61980
62106
|
page.seedCurrentUrl(pendingSeedUrl ?? info.url ?? "");
|
|
61981
62107
|
this.installFrameEventBridges(sessionId, page);
|
|
62108
|
+
this.adoptPendingOopifsByParent();
|
|
61982
62109
|
await this.applyInitScriptsToPage(page, { seedOnly: scriptsInstalled });
|
|
61983
62110
|
return;
|
|
61984
62111
|
}
|
|
61985
62112
|
try {
|
|
61986
62113
|
const { frameTree } = await session.send("Page.getFrameTree");
|
|
61987
62114
|
const childMainId = frameTree.frame.id;
|
|
61988
|
-
|
|
61989
|
-
|
|
61990
|
-
for (const p of this.pagesByTarget.values()) if ((function find(n) {
|
|
61991
|
-
if (n.frame.id === childMainId) return true;
|
|
61992
|
-
for (const c of n.childFrames ?? []) if (find(c)) return true;
|
|
61993
|
-
return false;
|
|
61994
|
-
})(p.asProtocolFrameTree(p.mainFrameId()))) {
|
|
61995
|
-
owner = p;
|
|
61996
|
-
break;
|
|
61997
|
-
}
|
|
61998
|
-
}
|
|
62115
|
+
const parentFrameId = frameTree.frame.parentId;
|
|
62116
|
+
const owner = this.findPageOwningFrame(childMainId) ?? (parentFrameId ? this.findPageOwningFrame(parentFrameId) : void 0);
|
|
61999
62117
|
if (owner) {
|
|
62000
|
-
|
|
62001
|
-
this.
|
|
62002
|
-
|
|
62003
|
-
|
|
62004
|
-
|
|
62118
|
+
this.adoptOopif(owner, session, childMainId);
|
|
62119
|
+
this.adoptPendingOopifsByParent();
|
|
62120
|
+
} else {
|
|
62121
|
+
this.pendingOopifByMainFrame.set(childMainId, sessionId);
|
|
62122
|
+
if (parentFrameId) this.pendingOopifParentByMainFrame.set(childMainId, parentFrameId);
|
|
62123
|
+
}
|
|
62005
62124
|
} catch {}
|
|
62006
62125
|
} finally {
|
|
62007
62126
|
await resume();
|
|
62008
62127
|
}
|
|
62009
62128
|
}
|
|
62129
|
+
/**
|
|
62130
|
+
* Page whose frame graph contains `frameId`, including adopted OOPIF roots.
|
|
62131
|
+
*/
|
|
62132
|
+
findPageOwningFrame(frameId) {
|
|
62133
|
+
const known = this.frameOwnerPage.get(frameId);
|
|
62134
|
+
if (known) {
|
|
62135
|
+
if (this.pagesByTarget.get(known.targetId()) === known && known.registry.frames.has(frameId)) return known;
|
|
62136
|
+
this.frameOwnerPage.delete(frameId);
|
|
62137
|
+
}
|
|
62138
|
+
for (const p of this.pagesByTarget.values()) if (p.listAllFrameIds().includes(frameId)) return p;
|
|
62139
|
+
}
|
|
62140
|
+
adoptOopif(owner, session, childMainId) {
|
|
62141
|
+
owner.adoptOopifSession(session, childMainId, () => {
|
|
62142
|
+
this.adoptPendingOopifsByParent();
|
|
62143
|
+
});
|
|
62144
|
+
this.adoptedOopifSessionByFrame.set(childMainId, session.id);
|
|
62145
|
+
this.sessionOwnerPage.set(session.id, owner);
|
|
62146
|
+
this.frameOwnerPage.set(childMainId, owner);
|
|
62147
|
+
this.installFrameEventBridges(session.id, owner);
|
|
62148
|
+
executionContexts.waitForMainWorld(session, childMainId).catch(() => {});
|
|
62149
|
+
}
|
|
62150
|
+
removePendingOopif(frameId) {
|
|
62151
|
+
this.pendingOopifByMainFrame.delete(frameId);
|
|
62152
|
+
this.pendingOopifParentByMainFrame.delete(frameId);
|
|
62153
|
+
}
|
|
62154
|
+
removePendingOopifsForPage(page) {
|
|
62155
|
+
const pageFrameIds = /* @__PURE__ */ new Set([page.mainFrameId(), ...page.registry.frames.keys()]);
|
|
62156
|
+
for (const [frameId, owner] of this.frameOwnerPage) if (owner === page) pageFrameIds.add(frameId);
|
|
62157
|
+
let removed;
|
|
62158
|
+
do {
|
|
62159
|
+
removed = false;
|
|
62160
|
+
for (const [frameId] of this.pendingOopifByMainFrame) {
|
|
62161
|
+
const parentId = this.pendingOopifParentByMainFrame.get(frameId);
|
|
62162
|
+
if (!pageFrameIds.has(frameId) && (!parentId || !pageFrameIds.has(parentId))) continue;
|
|
62163
|
+
pageFrameIds.add(frameId);
|
|
62164
|
+
this.removePendingOopif(frameId);
|
|
62165
|
+
removed = true;
|
|
62166
|
+
}
|
|
62167
|
+
} while (removed);
|
|
62168
|
+
}
|
|
62169
|
+
/**
|
|
62170
|
+
* An OOPIF that already existed when we attached (e.g. reconnecting to a live
|
|
62171
|
+
* browser, or a nested OOPIF) never emits `frameAttached` on its parent, so the
|
|
62172
|
+
* pending map would hold it forever. Adopt it once the page owning its parent is known.
|
|
62173
|
+
*/
|
|
62174
|
+
adoptPendingOopifsByParent() {
|
|
62175
|
+
let adopted = true;
|
|
62176
|
+
while (adopted) {
|
|
62177
|
+
adopted = false;
|
|
62178
|
+
for (const [childMainId, parentFrameId] of Array.from(this.pendingOopifParentByMainFrame.entries())) {
|
|
62179
|
+
const childSessionId = this.pendingOopifByMainFrame.get(childMainId);
|
|
62180
|
+
const child = childSessionId ? this.conn.getSession(childSessionId) : void 0;
|
|
62181
|
+
if (!child) {
|
|
62182
|
+
this.removePendingOopif(childMainId);
|
|
62183
|
+
continue;
|
|
62184
|
+
}
|
|
62185
|
+
const owner = this.findPageOwningFrame(parentFrameId);
|
|
62186
|
+
if (!owner) continue;
|
|
62187
|
+
this.removePendingOopif(childMainId);
|
|
62188
|
+
this.adoptOopif(owner, child, childMainId);
|
|
62189
|
+
adopted = true;
|
|
62190
|
+
}
|
|
62191
|
+
}
|
|
62192
|
+
}
|
|
62010
62193
|
async closePopupIfBlockedByDomainPolicy(info, source) {
|
|
62011
62194
|
if (!this.domainPolicy || !isTopLevelPage(info)) return false;
|
|
62012
62195
|
if (!info.openerId && !info.openerFrameId) return false;
|
|
@@ -62055,13 +62238,16 @@ var BrowserContext = class BrowserContext {
|
|
|
62055
62238
|
* - Drop any staged child for this session.
|
|
62056
62239
|
*/
|
|
62057
62240
|
onDetachedFromTarget(sessionId, targetId) {
|
|
62241
|
+
const closingPage = targetId ? this.pagesByTarget.get(targetId) : void 0;
|
|
62242
|
+
if (closingPage) this.removePendingOopifsForPage(closingPage);
|
|
62058
62243
|
const owner = this.sessionOwnerPage.get(sessionId);
|
|
62059
62244
|
if (owner) {
|
|
62060
62245
|
owner.detachOopifSession(sessionId);
|
|
62061
62246
|
this.sessionOwnerPage.delete(sessionId);
|
|
62062
62247
|
}
|
|
62063
62248
|
if (targetId && this.pagesByTarget.has(targetId)) this.cleanupByTarget(targetId);
|
|
62064
|
-
for (const [fid, sid] of Array.from(this.pendingOopifByMainFrame.entries())) if (sid === sessionId) this.
|
|
62249
|
+
for (const [fid, sid] of Array.from(this.pendingOopifByMainFrame.entries())) if (sid === sessionId) this.removePendingOopif(fid);
|
|
62250
|
+
for (const [fid, sid] of Array.from(this.adoptedOopifSessionByFrame.entries())) if (sid === sessionId) this.adoptedOopifSessionByFrame.delete(fid);
|
|
62065
62251
|
this._targetSessionListeners.delete(sessionId);
|
|
62066
62252
|
const session = this.conn.getSession(sessionId);
|
|
62067
62253
|
if (session) this.uninstallDomainPolicyHandler(session);
|
|
@@ -62075,15 +62261,12 @@ var BrowserContext = class BrowserContext {
|
|
|
62075
62261
|
this.recordPageCreationFailure(targetId, /* @__PURE__ */ new Error(`Target closed before page registration (${targetId})`), "Target closed before page registration");
|
|
62076
62262
|
const page = this.pagesByTarget.get(targetId);
|
|
62077
62263
|
if (!page) return;
|
|
62264
|
+
this.removePendingOopifsForPage(page);
|
|
62078
62265
|
page.dispose();
|
|
62079
62266
|
const mainId = page.mainFrameId();
|
|
62080
62267
|
this.mainFrameToTarget.delete(mainId);
|
|
62081
|
-
this.frameOwnerPage.delete(
|
|
62268
|
+
for (const [frameId, owner] of this.frameOwnerPage) if (owner === page) this.frameOwnerPage.delete(frameId);
|
|
62082
62269
|
for (const [sid, p] of Array.from(this.sessionOwnerPage.entries())) if (p === page) this.sessionOwnerPage.delete(sid);
|
|
62083
|
-
for (const [fid] of Array.from(this.pendingOopifByMainFrame.entries())) {
|
|
62084
|
-
const owner = this.frameOwnerPage.get(fid);
|
|
62085
|
-
if (!owner || owner === page) this.pendingOopifByMainFrame.delete(fid);
|
|
62086
|
-
}
|
|
62087
62270
|
this.pagesByTarget.delete(targetId);
|
|
62088
62271
|
this.createdAtByTarget.delete(targetId);
|
|
62089
62272
|
this.typeByTarget.delete(targetId);
|
|
@@ -62096,23 +62279,21 @@ var BrowserContext = class BrowserContext {
|
|
|
62096
62279
|
}
|
|
62097
62280
|
/**
|
|
62098
62281
|
* Wire Page-domain frame events for a session into the owning Page & mappings.
|
|
62099
|
-
*
|
|
62282
|
+
* Attachments preserve live adopted OOPIF ownership while updating topology.
|
|
62100
62283
|
*/
|
|
62101
62284
|
installFrameEventBridges(sessionId, owner) {
|
|
62102
62285
|
const session = this.conn.getSession(sessionId);
|
|
62103
62286
|
if (!session) return;
|
|
62104
62287
|
session.on("Page.frameAttached", (evt) => {
|
|
62105
62288
|
const { frameId, parentFrameId } = evt;
|
|
62106
|
-
|
|
62289
|
+
const adoptedSessionId = this.adoptedOopifSessionByFrame.get(frameId);
|
|
62290
|
+
const adoptedSession = adoptedSessionId ? this.conn.getSession(adoptedSessionId) : void 0;
|
|
62291
|
+
owner.onFrameAttached(frameId, parentFrameId ?? null, adoptedSession ?? session);
|
|
62107
62292
|
const pendingChildSessionId = this.pendingOopifByMainFrame.get(frameId);
|
|
62108
62293
|
if (pendingChildSessionId) {
|
|
62109
62294
|
const child = this.conn.getSession(pendingChildSessionId);
|
|
62110
|
-
|
|
62111
|
-
|
|
62112
|
-
this.sessionOwnerPage.set(child.id, owner);
|
|
62113
|
-
this.installFrameEventBridges(pendingChildSessionId, owner);
|
|
62114
|
-
}
|
|
62115
|
-
this.pendingOopifByMainFrame.delete(frameId);
|
|
62295
|
+
this.removePendingOopif(frameId);
|
|
62296
|
+
if (child) this.adoptOopif(owner, child, frameId);
|
|
62116
62297
|
}
|
|
62117
62298
|
this.frameOwnerPage.set(frameId, owner);
|
|
62118
62299
|
if (!parentFrameId) {
|
|
@@ -62121,6 +62302,7 @@ var BrowserContext = class BrowserContext {
|
|
|
62121
62302
|
if (topTargetId) this.mainFrameToTarget.set(newRoot, topTargetId);
|
|
62122
62303
|
this.frameOwnerPage.set(newRoot, owner);
|
|
62123
62304
|
}
|
|
62305
|
+
this.adoptPendingOopifsByParent();
|
|
62124
62306
|
});
|
|
62125
62307
|
session.on("Page.frameDetached", (evt) => {
|
|
62126
62308
|
owner.onFrameDetached(evt.frameId, evt.reason ?? "remove");
|
|
@@ -62210,7 +62392,7 @@ var STAGEHAND_RUNTIME_VERSION = package_default.version;
|
|
|
62210
62392
|
function startStagehandServiceWorker(scope = globalThis, runtime) {
|
|
62211
62393
|
const chromeRuntimeClient = new ChromeRuntimeClient(scope, STAGEHAND_SEND_TO_HOST_BINDING);
|
|
62212
62394
|
let rpcClient;
|
|
62213
|
-
|
|
62395
|
+
const activeRuntime = runtime ?? createStagehandRuntime({
|
|
62214
62396
|
browserSessionFactory: async (cdpUrl, logger, bootstrapLogger) => {
|
|
62215
62397
|
const locatorRuntimeResponse = await fetch(chrome.runtime.getURL("content-script.js"));
|
|
62216
62398
|
if (!locatorRuntimeResponse.ok) throw new Error(`Failed to load Stagehand locator runtime: ${locatorRuntimeResponse.status}`);
|
|
@@ -62246,7 +62428,32 @@ function startStagehandServiceWorker(scope = globalThis, runtime) {
|
|
|
62246
62428
|
if (!rpcClient) throw new Error("Stagehand RPC client is not connected");
|
|
62247
62429
|
return await rpcClient.send(StagehandMethods.llmGenerate, params);
|
|
62248
62430
|
}
|
|
62249
|
-
})
|
|
62431
|
+
});
|
|
62432
|
+
rpcClient = new RPCClient(chromeRuntimeClient, new RPCRouter(activeRuntime));
|
|
62433
|
+
const lifecycle = new ClientLifecycle({
|
|
62434
|
+
connection: async (cdpUrl) => {
|
|
62435
|
+
await activeRuntime.ensureBrowserConnection(cdpUrl);
|
|
62436
|
+
const session = activeRuntime.browserSession;
|
|
62437
|
+
if (!(session instanceof BrowserContext)) throw new Error("Stagehand client lifecycle requires a CDP browser connection");
|
|
62438
|
+
return session.conn;
|
|
62439
|
+
},
|
|
62440
|
+
rejectClientCallbacks: () => rpcClient?.releaseClient(),
|
|
62441
|
+
closeConnection: () => activeRuntime.dropBrowserConnection(),
|
|
62442
|
+
disposeClient: () => activeRuntime.disposeStagehandInstance(),
|
|
62443
|
+
onError: (error) => {
|
|
62444
|
+
console.error("Stagehand client lifecycle cleanup failed", error);
|
|
62445
|
+
}
|
|
62446
|
+
});
|
|
62447
|
+
scope.__stagehandClientLifecycle = async (input) => {
|
|
62448
|
+
const action = typeof input === "object" && input !== null && "action" in input ? input.action : void 0;
|
|
62449
|
+
try {
|
|
62450
|
+
await lifecycle.handle(input);
|
|
62451
|
+
} catch (error) {
|
|
62452
|
+
if (action === "detach") rpcClient?.acceptClient();
|
|
62453
|
+
throw error;
|
|
62454
|
+
}
|
|
62455
|
+
if (action === "attach") rpcClient?.acceptClient();
|
|
62456
|
+
};
|
|
62250
62457
|
scope.__stagehand_runtime = {
|
|
62251
62458
|
protocolVersion: STAGEHAND_PROTOCOL_VERSION,
|
|
62252
62459
|
serverInfo: {
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_generated/protocol_version.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"""Generated from packages/protocol/package.json. Do not edit."""
|
|
2
2
|
|
|
3
3
|
PROTOCOL_SEMVER_PATTERN = '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$'
|
|
4
|
-
STAGEHAND_PROTOCOL_VERSION = '3.
|
|
4
|
+
STAGEHAND_PROTOCOL_VERSION = '3.1.1-alpha-44d99dfbdd24baaf06986492792348582266d8f0'
|
|
@@ -107,26 +107,54 @@ class ResolvedBrowserSource:
|
|
|
107
107
|
return
|
|
108
108
|
if self._close_task is None:
|
|
109
109
|
self._close_task = asyncio.create_task(_invoke_close(self._close_callback))
|
|
110
|
-
|
|
110
|
+
task = self._close_task
|
|
111
|
+
try:
|
|
112
|
+
await asyncio.shield(task)
|
|
113
|
+
except BaseException:
|
|
114
|
+
# A failed close (e.g. a transient Browserbase release error) can be retried.
|
|
115
|
+
if (
|
|
116
|
+
self._close_task is task
|
|
117
|
+
and task.done()
|
|
118
|
+
and not task.cancelled()
|
|
119
|
+
and task.exception() is not None
|
|
120
|
+
):
|
|
121
|
+
self._close_task = None
|
|
122
|
+
raise
|
|
111
123
|
|
|
112
124
|
|
|
113
125
|
async def _invoke_close(callback: Callable[[], Awaitable[None]]) -> None:
|
|
114
126
|
await callback()
|
|
115
127
|
|
|
116
128
|
|
|
129
|
+
async def _noop() -> None:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
117
133
|
@dataclass(frozen=True)
|
|
118
134
|
class _ChromeProfile:
|
|
119
135
|
path: Path
|
|
120
136
|
remove: bool
|
|
121
137
|
|
|
122
138
|
|
|
139
|
+
_BROWSER_CLOSE_TIMEOUT_SECONDS = 5.0
|
|
140
|
+
_DISCONNECT_STAGEHAND_TIMEOUT_SECONDS = 10.0
|
|
141
|
+
_BROWSER_DISCONNECTED_MESSAGE = "The browser is disconnected; connect() again to close it"
|
|
142
|
+
|
|
143
|
+
|
|
123
144
|
class StagehandBrowser:
|
|
124
145
|
__slots__ = (
|
|
125
146
|
"_attachment",
|
|
126
147
|
"_claimed",
|
|
148
|
+
"_close_stagehand",
|
|
127
149
|
"_close_callback",
|
|
150
|
+
"_disconnect_callback",
|
|
128
151
|
"_invalidate_callback",
|
|
129
|
-
"
|
|
152
|
+
"_connected",
|
|
153
|
+
"_close_without_connection",
|
|
154
|
+
"_cdp_url",
|
|
155
|
+
"_state",
|
|
156
|
+
"_disconnect_task",
|
|
157
|
+
"_close_task",
|
|
130
158
|
"_context",
|
|
131
159
|
"_origin",
|
|
132
160
|
"_provider",
|
|
@@ -141,7 +169,11 @@ class StagehandBrowser:
|
|
|
141
169
|
close: Callable[[], Awaitable[None]],
|
|
142
170
|
*,
|
|
143
171
|
invalidate: Callable[[], Awaitable[None]] | None = None,
|
|
172
|
+
disconnect: Callable[[], Awaitable[None]] | None = None,
|
|
173
|
+
connected: Callable[[], bool] | None = None,
|
|
174
|
+
close_without_connection: Callable[[], Awaitable[None]] | None = None,
|
|
144
175
|
session_id: str | None = None,
|
|
176
|
+
cdp_url: str | None = None,
|
|
145
177
|
_token: object | None = None,
|
|
146
178
|
) -> None:
|
|
147
179
|
if _token is not _BROWSER_TOKEN:
|
|
@@ -151,9 +183,18 @@ class StagehandBrowser:
|
|
|
151
183
|
self._attachment = attachment
|
|
152
184
|
self._close_callback = close
|
|
153
185
|
self._invalidate_callback = invalidate or close
|
|
186
|
+
self._disconnect_callback = disconnect or _noop
|
|
187
|
+
self._connected = connected or (lambda: True)
|
|
188
|
+
# What close() does after an unexpectedly lost connection.
|
|
189
|
+
self._close_without_connection = close_without_connection or _noop
|
|
190
|
+
self._cdp_url = cdp_url
|
|
154
191
|
self._claimed = False
|
|
155
|
-
self.
|
|
192
|
+
self._state: Literal["open", "disconnected", "closed"] = "open"
|
|
193
|
+
self._disconnect_task: asyncio.Task[None] | None = None
|
|
194
|
+
self._close_task: asyncio.Task[None] | None = None
|
|
156
195
|
self._context: BrowserContext | None = None
|
|
196
|
+
# Closes the attached Stagehand instance; disconnect() runs it before the connection.
|
|
197
|
+
self._close_stagehand: Callable[[], Awaitable[None]] | None = None
|
|
157
198
|
self._session_id = session_id
|
|
158
199
|
|
|
159
200
|
@property
|
|
@@ -169,9 +210,24 @@ class StagehandBrowser:
|
|
|
169
210
|
"""Browserbase session id backing this browser; None for local browsers."""
|
|
170
211
|
return self._session_id
|
|
171
212
|
|
|
213
|
+
@property
|
|
214
|
+
def cdp_url(self) -> str | None:
|
|
215
|
+
"""CDP URL to reconnect with local_browser.connect(); None for Browserbase browsers."""
|
|
216
|
+
return self._cdp_url
|
|
217
|
+
|
|
172
218
|
@property
|
|
173
219
|
def closed(self) -> bool:
|
|
174
|
-
|
|
220
|
+
"""True once ``close()`` or ``disconnect()`` was called."""
|
|
221
|
+
return self._state != "open"
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def connected(self) -> bool:
|
|
225
|
+
"""False once the handle is closed or disconnected, or its connection was lost.
|
|
226
|
+
|
|
227
|
+
After a lost connection ``close()`` can still have work to do, such as releasing a
|
|
228
|
+
Browserbase session.
|
|
229
|
+
"""
|
|
230
|
+
return self._state == "open" and self._connected()
|
|
175
231
|
|
|
176
232
|
@property
|
|
177
233
|
def context(self) -> BrowserContext:
|
|
@@ -183,18 +239,81 @@ class StagehandBrowser:
|
|
|
183
239
|
return self._context
|
|
184
240
|
|
|
185
241
|
def close(self) -> Awaitable[None]:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
242
|
+
"""Close the browser.
|
|
243
|
+
|
|
244
|
+
A local browser receives ``Browser.close`` (a launched one is also terminated if
|
|
245
|
+
it keeps running, and its temporary profile removed unless preserved), and a
|
|
246
|
+
Browserbase session is released. After ``disconnect()`` it raises: ``connect()``
|
|
247
|
+
again to close the browser. After an unexpectedly lost connection it still
|
|
248
|
+
releases a Browserbase session and does nothing for a local browser. Repeated
|
|
249
|
+
``close()`` calls do nothing.
|
|
250
|
+
"""
|
|
251
|
+
if self._close_task is None:
|
|
252
|
+
if self._state == "disconnected":
|
|
253
|
+
raise RuntimeError(_BROWSER_DISCONNECTED_MESSAGE)
|
|
254
|
+
self._close_task = asyncio.create_task(
|
|
255
|
+
_run_browser_terminal_callback(self._close_callback)
|
|
256
|
+
if self._connected()
|
|
257
|
+
else self._close_without_connection_retryable(),
|
|
189
258
|
name="stagehand-browser-close",
|
|
190
259
|
)
|
|
191
|
-
|
|
260
|
+
self._state = "closed"
|
|
261
|
+
return asyncio.shield(self._close_task)
|
|
262
|
+
|
|
263
|
+
async def _close_without_connection_retryable(self) -> None:
|
|
264
|
+
try:
|
|
265
|
+
await self._close_without_connection()
|
|
266
|
+
except BaseException:
|
|
267
|
+
# Releasing over the Browserbase API can fail transiently: allow a retry.
|
|
268
|
+
self._close_task = None
|
|
269
|
+
raise
|
|
270
|
+
|
|
271
|
+
def disconnect(self) -> Awaitable[None]:
|
|
272
|
+
"""Close the attached Stagehand instance, then Stagehand's connection.
|
|
273
|
+
|
|
274
|
+
The attached instance closes first, including the extension's own connection to
|
|
275
|
+
the browser. The browser keeps running: Stagehand never closes it or releases its
|
|
276
|
+
Browserbase session here. Browserbase still ends a session without
|
|
277
|
+
``keep_alive`` when its CDP connection closes, so only ``keep_alive`` sessions
|
|
278
|
+
survive a disconnect. The handle is finished afterwards: ``connect()`` again for
|
|
279
|
+
a new one. Repeated ``disconnect()`` calls, or ``disconnect()`` after
|
|
280
|
+
``close()``, do nothing.
|
|
281
|
+
"""
|
|
282
|
+
if self._state == "closed" and self._close_task is not None:
|
|
283
|
+
return asyncio.shield(self._close_task)
|
|
284
|
+
if self._disconnect_task is None:
|
|
285
|
+
self._state = "disconnected"
|
|
286
|
+
self._disconnect_task = asyncio.create_task(
|
|
287
|
+
self._disconnect_after_closing_stagehand(self._close_stagehand),
|
|
288
|
+
name="stagehand-browser-disconnect",
|
|
289
|
+
)
|
|
290
|
+
return asyncio.shield(self._disconnect_task)
|
|
291
|
+
|
|
292
|
+
async def _disconnect_after_closing_stagehand(
|
|
293
|
+
self, close_stagehand: Callable[[], Awaitable[None]] | None
|
|
294
|
+
) -> None:
|
|
295
|
+
try:
|
|
296
|
+
try:
|
|
297
|
+
if close_stagehand is not None:
|
|
298
|
+
# stagehand.close() waits for in-flight work without a deadline;
|
|
299
|
+
# disconnect() stops waiting and lets go anyway, which also ends that work.
|
|
300
|
+
await _close_stagehand_within(
|
|
301
|
+
close_stagehand, _DISCONNECT_STAGEHAND_TIMEOUT_SECONDS
|
|
302
|
+
)
|
|
303
|
+
finally:
|
|
304
|
+
await self._disconnect_callback()
|
|
305
|
+
except BaseException:
|
|
306
|
+
# Still connected means nothing was let go: keep the handle usable for a retry.
|
|
307
|
+
if self._connected():
|
|
308
|
+
self._state = "open"
|
|
309
|
+
self._disconnect_task = None
|
|
310
|
+
raise
|
|
192
311
|
|
|
193
312
|
|
|
194
313
|
def _claim_browser(browser: object) -> _ClaimedBrowser:
|
|
195
314
|
if not isinstance(browser, StagehandBrowser):
|
|
196
315
|
raise TypeError("browser must be created by local_browser or browserbase")
|
|
197
|
-
if browser.
|
|
316
|
+
if not browser.connected:
|
|
198
317
|
raise RuntimeError("Cannot attach Stagehand to a closed browser")
|
|
199
318
|
if browser._claimed:
|
|
200
319
|
raise RuntimeError("This browser is already attached to a Stagehand instance")
|
|
@@ -211,12 +330,15 @@ def _release_browser(browser: StagehandBrowser) -> None:
|
|
|
211
330
|
def _invalidate_browser(browser: StagehandBrowser) -> Awaitable[None]:
|
|
212
331
|
if not isinstance(browser, StagehandBrowser):
|
|
213
332
|
raise TypeError("browser must be created by local_browser or browserbase")
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
333
|
+
existing = browser._close_task or browser._disconnect_task
|
|
334
|
+
if existing is not None:
|
|
335
|
+
return asyncio.shield(existing)
|
|
336
|
+
browser._state = "closed"
|
|
337
|
+
browser._close_task = asyncio.create_task(
|
|
338
|
+
_run_browser_terminal_callback(browser._invalidate_callback),
|
|
339
|
+
name="stagehand-browser-invalidate",
|
|
340
|
+
)
|
|
341
|
+
return asyncio.shield(browser._close_task)
|
|
220
342
|
|
|
221
343
|
|
|
222
344
|
async def _run_browser_terminal_callback(
|
|
@@ -225,19 +347,35 @@ async def _run_browser_terminal_callback(
|
|
|
225
347
|
await callback()
|
|
226
348
|
|
|
227
349
|
|
|
350
|
+
async def _close_stagehand_within(
|
|
351
|
+
close_stagehand: Callable[[], Awaitable[None]], timeout: float
|
|
352
|
+
) -> None:
|
|
353
|
+
deadline = asyncio.timeout(timeout)
|
|
354
|
+
try:
|
|
355
|
+
async with deadline:
|
|
356
|
+
await close_stagehand()
|
|
357
|
+
except TimeoutError:
|
|
358
|
+
# Only this deadline is expected; a TimeoutError from Stagehand itself is a failure.
|
|
359
|
+
if not deadline.expired():
|
|
360
|
+
raise
|
|
361
|
+
|
|
362
|
+
|
|
228
363
|
def _attach_browser_context(
|
|
229
364
|
browser: StagehandBrowser,
|
|
230
365
|
context: BrowserContext,
|
|
366
|
+
close_stagehand: Callable[[], Awaitable[None]] | None = None,
|
|
231
367
|
) -> None:
|
|
232
368
|
if not browser._claimed:
|
|
233
369
|
raise RuntimeError("Cannot attach a browser context before Stagehand claims the browser")
|
|
234
370
|
if browser._context is not None:
|
|
235
371
|
raise RuntimeError("This browser already has a Stagehand context")
|
|
236
372
|
browser._context = context
|
|
373
|
+
browser._close_stagehand = close_stagehand
|
|
237
374
|
|
|
238
375
|
|
|
239
376
|
def _detach_browser_context(browser: StagehandBrowser) -> None:
|
|
240
377
|
browser._context = None
|
|
378
|
+
browser._close_stagehand = None
|
|
241
379
|
|
|
242
380
|
|
|
243
381
|
def _browser_session_metadata(
|
|
@@ -325,6 +463,10 @@ async def _connect_browser(
|
|
|
325
463
|
extension_id=extension_id,
|
|
326
464
|
preloaded_extension=preloaded_extension,
|
|
327
465
|
service_worker_url_includes="service-worker.js",
|
|
466
|
+
# Only a local browser Stagehand launched without keep_alive is shut down when
|
|
467
|
+
# this client disappears; Browserbase applies its own keep_alive policy.
|
|
468
|
+
close_browser_on_disconnect=provider == "local" and owns_source,
|
|
469
|
+
extension_cdp_url=worker_init_metadata.browser_cdp_url,
|
|
328
470
|
)
|
|
329
471
|
if after_connect is not None:
|
|
330
472
|
await after_connect(cdp_client)
|
|
@@ -354,6 +496,13 @@ async def _connect_browser(
|
|
|
354
496
|
|
|
355
497
|
connected_client = cdp_client
|
|
356
498
|
|
|
499
|
+
async def disconnect() -> None:
|
|
500
|
+
# Detaching first disarms the extension's watchdog; without that acknowledgement
|
|
501
|
+
# the socket stays open, since closing it would read as a lost client and could
|
|
502
|
+
# shut the browser down.
|
|
503
|
+
await connected_client.detach_client()
|
|
504
|
+
await connected_client.close()
|
|
505
|
+
|
|
357
506
|
async def invalidate() -> None:
|
|
358
507
|
try:
|
|
359
508
|
await connected_client.close()
|
|
@@ -364,12 +513,28 @@ async def _connect_browser(
|
|
|
364
513
|
async def close() -> None:
|
|
365
514
|
errors: list[BaseException] = []
|
|
366
515
|
try:
|
|
367
|
-
if provider == "local"
|
|
516
|
+
if provider == "local":
|
|
368
517
|
try:
|
|
369
|
-
|
|
518
|
+
# Bounded so an unresponsive Chrome cannot keep a launched one from
|
|
519
|
+
# being terminated.
|
|
520
|
+
async with asyncio.timeout(_BROWSER_CLOSE_TIMEOUT_SECONDS):
|
|
521
|
+
await connected_client.send_command("Browser.close")
|
|
370
522
|
except CDPConnectionClosedError:
|
|
371
523
|
pass
|
|
372
|
-
|
|
524
|
+
except TimeoutError:
|
|
525
|
+
# A launched browser is terminated below; a connected one cannot be.
|
|
526
|
+
if origin == "connected":
|
|
527
|
+
raise RuntimeError(
|
|
528
|
+
"Browser.close did not respond within "
|
|
529
|
+
f"{_BROWSER_CLOSE_TIMEOUT_SECONDS} seconds"
|
|
530
|
+
) from None
|
|
531
|
+
except BaseException as error:
|
|
532
|
+
errors.append(error)
|
|
533
|
+
try:
|
|
534
|
+
# A launched local browser is also terminated if Browser.close failed or
|
|
535
|
+
# left it running, and its profile removed unless preserved; a Browserbase
|
|
536
|
+
# session is released.
|
|
537
|
+
if provider == "browserbase" or origin == "launched":
|
|
373
538
|
await source.close()
|
|
374
539
|
except BaseException as error:
|
|
375
540
|
errors.append(error)
|
|
@@ -391,6 +556,14 @@ async def _connect_browser(
|
|
|
391
556
|
),
|
|
392
557
|
close,
|
|
393
558
|
invalidate=invalidate,
|
|
559
|
+
# Never closes the browser or releases its session. Browserbase still ends a
|
|
560
|
+
# session without keep_alive once this connection closes.
|
|
561
|
+
disconnect=disconnect,
|
|
562
|
+
connected=lambda: not connected_client.closed,
|
|
563
|
+
# After an unexpected loss, a Browserbase session can still be released over its
|
|
564
|
+
# API; a local browser is left alone.
|
|
565
|
+
close_without_connection=source.close if provider == "browserbase" else None,
|
|
566
|
+
cdp_url=source.cdp_url if provider == "local" else None,
|
|
394
567
|
session_id=(
|
|
395
568
|
worker_init_metadata.browser.session_id if worker_init_metadata.browser else None
|
|
396
569
|
),
|
|
@@ -15,7 +15,7 @@ from urllib.request import HTTPRedirectHandler, Request, build_opener, urlopen
|
|
|
15
15
|
|
|
16
16
|
from typing_extensions import override
|
|
17
17
|
from websockets.asyncio.client import connect as WebSocketConnect
|
|
18
|
-
from websockets.exceptions import InvalidStatus
|
|
18
|
+
from websockets.exceptions import ConnectionClosed, InvalidStatus
|
|
19
19
|
|
|
20
20
|
from stagehand._generated.protocol_version import (
|
|
21
21
|
PROTOCOL_SEMVER_PATTERN,
|
|
@@ -239,6 +239,9 @@ class CDPConnectionClosedError(RuntimeError):
|
|
|
239
239
|
super().__init__("CDP connection closed")
|
|
240
240
|
|
|
241
241
|
|
|
242
|
+
_CLIENT_TARGET_URL = "about:blank#stagehand-client-connection"
|
|
243
|
+
|
|
244
|
+
|
|
242
245
|
class CDPClient:
|
|
243
246
|
def __init__(
|
|
244
247
|
self,
|
|
@@ -253,6 +256,9 @@ class CDPClient:
|
|
|
253
256
|
self._session_id: str | None = None
|
|
254
257
|
self._service_worker: ServiceWorkerInfo | None = None
|
|
255
258
|
self._closed = False
|
|
259
|
+
self._client_target_id: str | None = None
|
|
260
|
+
# Whether the extension shuts the browser down if this client is lost.
|
|
261
|
+
self._client_close_browser_on_loss = False
|
|
256
262
|
self._background_tasks: set[asyncio.Task[dict[str, object]]] = set()
|
|
257
263
|
self._reader = asyncio.create_task(self._read(), name="stagehand-cdp-reader")
|
|
258
264
|
|
|
@@ -267,6 +273,8 @@ class CDPClient:
|
|
|
267
273
|
preloaded_extension: bool = False,
|
|
268
274
|
service_worker_url_includes: str | None = None,
|
|
269
275
|
headers: Mapping[str, str] | None = None,
|
|
276
|
+
close_browser_on_disconnect: bool | None = None,
|
|
277
|
+
extension_cdp_url: str | None = None,
|
|
270
278
|
) -> CDPClient:
|
|
271
279
|
"""Connect without an independent lifecycle deadline.
|
|
272
280
|
|
|
@@ -338,17 +346,104 @@ class CDPClient:
|
|
|
338
346
|
session_id=session_id,
|
|
339
347
|
)
|
|
340
348
|
await client._wait_for_runtime_receiver(session_id)
|
|
349
|
+
if close_browser_on_disconnect is not None:
|
|
350
|
+
await client._register_client(
|
|
351
|
+
close_browser_on_disconnect,
|
|
352
|
+
extension_cdp_url or web_socket_debugger_url,
|
|
353
|
+
)
|
|
341
354
|
return client
|
|
342
355
|
except BaseException:
|
|
343
356
|
await asyncio.shield(client.close())
|
|
344
357
|
raise
|
|
345
358
|
|
|
359
|
+
@property
|
|
360
|
+
def closed(self) -> bool:
|
|
361
|
+
return self._closed
|
|
362
|
+
|
|
346
363
|
@property
|
|
347
364
|
def service_worker(self) -> ServiceWorkerInfo:
|
|
348
365
|
if self._service_worker is None:
|
|
349
366
|
raise RuntimeError("Stagehand service worker is not attached")
|
|
350
367
|
return self._service_worker
|
|
351
368
|
|
|
369
|
+
async def _register_client(self, close_browser_on_disconnect: bool, cdp_url: str) -> None:
|
|
370
|
+
marker = await self.send_command(
|
|
371
|
+
"Target.createTarget",
|
|
372
|
+
{"url": _CLIENT_TARGET_URL, "hidden": True, "background": True},
|
|
373
|
+
)
|
|
374
|
+
target_id = _required_string(marker, "targetId", "Target.createTarget")
|
|
375
|
+
# Chrome accepts unknown parameters: a build without hidden targets opens an
|
|
376
|
+
# ordinary tab that outlives this connection and would never signal its loss.
|
|
377
|
+
info = await self.send_command("Target.getTargetInfo", {"targetId": target_id})
|
|
378
|
+
target_info = info.get("targetInfo")
|
|
379
|
+
if not isinstance(target_info, dict) or target_info.get("type") != "other":
|
|
380
|
+
await self.send_command("Target.closeTarget", {"targetId": target_id})
|
|
381
|
+
raise RuntimeError(
|
|
382
|
+
"Stagehand requires Chrome 137 or later, which supports hidden targets"
|
|
383
|
+
)
|
|
384
|
+
if not await self._client_lifecycle_command({
|
|
385
|
+
"action": "attach",
|
|
386
|
+
"targetId": target_id,
|
|
387
|
+
"cdpUrl": cdp_url,
|
|
388
|
+
"closeBrowserOnDisconnect": close_browser_on_disconnect,
|
|
389
|
+
}):
|
|
390
|
+
# The protocol handshake already rejects older extensions; this one is
|
|
391
|
+
# inconsistent.
|
|
392
|
+
await self.send_command("Target.closeTarget", {"targetId": target_id})
|
|
393
|
+
raise RuntimeError(
|
|
394
|
+
"The Stagehand extension in this browser does not support the client "
|
|
395
|
+
"watchdog; update the extension"
|
|
396
|
+
)
|
|
397
|
+
self._client_target_id = target_id
|
|
398
|
+
self._client_close_browser_on_loss = close_browser_on_disconnect
|
|
399
|
+
|
|
400
|
+
async def detach_client(self) -> None:
|
|
401
|
+
"""Acknowledge intentional detach before the socket/hidden target disappears."""
|
|
402
|
+
if self._client_target_id is None or self._closed:
|
|
403
|
+
return
|
|
404
|
+
try:
|
|
405
|
+
async with asyncio.timeout(10):
|
|
406
|
+
acknowledged = await self._client_lifecycle_command({
|
|
407
|
+
"action": "detach",
|
|
408
|
+
"targetId": self._client_target_id,
|
|
409
|
+
})
|
|
410
|
+
except CDPConnectionClosedError:
|
|
411
|
+
# Browserbase ends a keep_alive=False session once the extension closes its own
|
|
412
|
+
# connection, which closes this one before the acknowledgement arrives: the
|
|
413
|
+
# detach happened. A failure or timeout with this connection open still raises,
|
|
414
|
+
# and so does a lost connection when the extension shuts the browser down on
|
|
415
|
+
# loss: it may not have received the detach.
|
|
416
|
+
if self._client_close_browser_on_loss:
|
|
417
|
+
raise
|
|
418
|
+
acknowledged = True
|
|
419
|
+
if not acknowledged:
|
|
420
|
+
raise RuntimeError("The Stagehand extension did not acknowledge the detach")
|
|
421
|
+
self._client_target_id = None
|
|
422
|
+
|
|
423
|
+
async def _client_lifecycle_command(self, params: dict[str, object]) -> bool:
|
|
424
|
+
"""Return False when the extension has no client lifecycle support."""
|
|
425
|
+
expression = (
|
|
426
|
+
"(async () => { "
|
|
427
|
+
'if (typeof globalThis.__stagehandClientLifecycle !== "function") return false; '
|
|
428
|
+
f"await globalThis.__stagehandClientLifecycle({json.dumps(params)}); "
|
|
429
|
+
"return true; })()"
|
|
430
|
+
)
|
|
431
|
+
result = await self.send_command(
|
|
432
|
+
"Runtime.evaluate",
|
|
433
|
+
{"expression": expression, "awaitPromise": True, "returnByValue": True},
|
|
434
|
+
session_id=self._session_id,
|
|
435
|
+
)
|
|
436
|
+
value = result.get("result")
|
|
437
|
+
if (
|
|
438
|
+
result.get("exceptionDetails")
|
|
439
|
+
or not isinstance(value, dict)
|
|
440
|
+
or not isinstance(value.get("value"), bool)
|
|
441
|
+
):
|
|
442
|
+
raise RuntimeError(
|
|
443
|
+
f"Stagehand client lifecycle failed: {result.get('exceptionDetails')}"
|
|
444
|
+
)
|
|
445
|
+
return bool(value["value"])
|
|
446
|
+
|
|
352
447
|
async def send(
|
|
353
448
|
self,
|
|
354
449
|
message: dict[str, object],
|
|
@@ -415,7 +510,10 @@ class CDPClient:
|
|
|
415
510
|
message["sessionId"] = session_id
|
|
416
511
|
|
|
417
512
|
try:
|
|
418
|
-
|
|
513
|
+
try:
|
|
514
|
+
await self._socket.send(json.dumps(message, separators=(",", ":")))
|
|
515
|
+
except ConnectionClosed as error:
|
|
516
|
+
raise CDPConnectionClosedError() from error
|
|
419
517
|
result = await response
|
|
420
518
|
except BaseException:
|
|
421
519
|
if not response.done():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_extension/content-script.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_generated/__init__.py
RENAMED
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/_generated/input_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_connection.py
RENAMED
|
File without changes
|
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_services.py
RENAMED
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_session.py
RENAMED
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1661}/src/stagehand/browserbase_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|