stagehand 4.3.1a0.dev1658__tar.gz → 4.4.0a0.dev1660__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.dev1660}/PKG-INFO +1 -1
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/pyproject.toml +1 -1
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/manifest.json +2 -2
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/service-worker.js +157 -8
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_generated/protocol_version.py +1 -1
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browser.py +191 -18
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/cdp_client.py +100 -2
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/stagehand.py +1 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/README.md +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/__init__.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/blank.html +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/content-script.js +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/wake-service-worker.html +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_extension/wake-service-worker.js +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_generated/__init__.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_generated/input_types.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_generated/models.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_sdk_identity.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/_validation.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browser_clipboard.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browser_context.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browserbase_connection.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browserbase_rpc.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browserbase_services.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browserbase_session.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browserbase_transport.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/client_models.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/client_types.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/extension_assets.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/file_upload.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/locator.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/page.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/py.typed +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/response.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/rpc_client.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/timeouts.py +0 -0
- {stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/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.dev1660
|
|
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.dev1660}/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.dev1660}/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-d59afde37e995b165a41f754a12a446f11d471a2",
|
|
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-d59afde37e995b165a41f754a12a446f11d471a2",
|
|
56388
56398
|
"private": true,
|
|
56389
56399
|
type: "module",
|
|
56390
56400
|
scripts: {
|
|
@@ -60379,6 +60389,7 @@ var StagehandRuntime = class {
|
|
|
60379
60389
|
this.state = createStore()(() => StagehandRuntimeStateSchema.parse({ status: "idle" }));
|
|
60380
60390
|
this.pagesById = /* @__PURE__ */ new Map();
|
|
60381
60391
|
this.pageEventSubscriptions = /* @__PURE__ */ new Map();
|
|
60392
|
+
this.browserConnectionGeneration = 0;
|
|
60382
60393
|
this.initializationInProgress = false;
|
|
60383
60394
|
this.lifecycleTail = Promise.resolve();
|
|
60384
60395
|
this.stagehandInstanceClosing = false;
|
|
@@ -60387,6 +60398,7 @@ var StagehandRuntime = class {
|
|
|
60387
60398
|
}
|
|
60388
60399
|
async replaceBrowserConnection(params, bootstrapLogger) {
|
|
60389
60400
|
const { cdpUrl } = params;
|
|
60401
|
+
const generation = ++this.browserConnectionGeneration;
|
|
60390
60402
|
const previousSession = this.browserSession;
|
|
60391
60403
|
this.browserSession = void 0;
|
|
60392
60404
|
this.browserSessionCdpUrl = void 0;
|
|
@@ -60395,14 +60407,35 @@ var StagehandRuntime = class {
|
|
|
60395
60407
|
this.responseHandles.clear();
|
|
60396
60408
|
await previousSession?.close();
|
|
60397
60409
|
try {
|
|
60398
|
-
|
|
60410
|
+
const session = await this.adapters.browserSessionFactory(cdpUrl, this.logger, bootstrapLogger);
|
|
60411
|
+
if (generation !== this.browserConnectionGeneration) {
|
|
60412
|
+
await session.close();
|
|
60413
|
+
throw new Error("Stagehand browser connection was dropped while connecting");
|
|
60414
|
+
}
|
|
60415
|
+
this.browserSession = session;
|
|
60399
60416
|
this.browserSessionCdpUrl = cdpUrl;
|
|
60400
60417
|
} catch (error) {
|
|
60401
|
-
|
|
60402
|
-
|
|
60418
|
+
if (generation === this.browserConnectionGeneration) {
|
|
60419
|
+
await this.browserSession?.close();
|
|
60420
|
+
this.browserSession = void 0;
|
|
60421
|
+
}
|
|
60403
60422
|
throw error;
|
|
60404
60423
|
}
|
|
60405
60424
|
}
|
|
60425
|
+
/**
|
|
60426
|
+
* Closes the runtime's own browser connection immediately, outside the lifecycle
|
|
60427
|
+
* queue: a lost client must not wait behind lifecycle work that needs that client.
|
|
60428
|
+
*/
|
|
60429
|
+
async dropBrowserConnection() {
|
|
60430
|
+
this.browserConnectionGeneration += 1;
|
|
60431
|
+
const session = this.browserSession;
|
|
60432
|
+
this.browserSession = void 0;
|
|
60433
|
+
this.browserSessionCdpUrl = void 0;
|
|
60434
|
+
await session?.close();
|
|
60435
|
+
}
|
|
60436
|
+
async ensureBrowserConnection(cdpUrl) {
|
|
60437
|
+
if (!this.browserSession?.connected || this.browserSessionCdpUrl !== cdpUrl) await this.replaceBrowserConnection({ cdpUrl });
|
|
60438
|
+
}
|
|
60406
60439
|
async initialize(params, logger = this.logger) {
|
|
60407
60440
|
if (this.initializationInProgress) throw new Error("Stagehand initialization is already in progress");
|
|
60408
60441
|
this.initializationInProgress = true;
|
|
@@ -61394,6 +61427,96 @@ function getDomainPolicyDecision(url, policy) {
|
|
|
61394
61427
|
};
|
|
61395
61428
|
return { action: "continue" };
|
|
61396
61429
|
}
|
|
61430
|
+
var RequestSchema = discriminatedUnion("action", [strictObject({
|
|
61431
|
+
action: literal("attach"),
|
|
61432
|
+
targetId: string().min(1),
|
|
61433
|
+
cdpUrl: string().min(1),
|
|
61434
|
+
closeBrowserOnDisconnect: boolean()
|
|
61435
|
+
}), strictObject({
|
|
61436
|
+
action: literal("detach"),
|
|
61437
|
+
targetId: string().min(1)
|
|
61438
|
+
})]);
|
|
61439
|
+
/** Browser-side ownership of one SDK attachment. No heartbeat or expiry timer. */
|
|
61440
|
+
var ClientLifecycle = class {
|
|
61441
|
+
constructor(options) {
|
|
61442
|
+
this.options = options;
|
|
61443
|
+
this.tail = Promise.resolve();
|
|
61444
|
+
}
|
|
61445
|
+
handle(input) {
|
|
61446
|
+
const request = RequestSchema.parse(input);
|
|
61447
|
+
return this.enqueue(async () => {
|
|
61448
|
+
if (request.action === "detach") {
|
|
61449
|
+
const attachment = this.attachment;
|
|
61450
|
+
if (attachment?.targetId !== request.targetId) {
|
|
61451
|
+
if (this.detachedTargetId === request.targetId) return;
|
|
61452
|
+
throw new Error("Stagehand client attachment is no longer active");
|
|
61453
|
+
}
|
|
61454
|
+
attachment.closeBrowserOnDisconnect = false;
|
|
61455
|
+
this.options.rejectClientCallbacks();
|
|
61456
|
+
await this.options.closeConnection();
|
|
61457
|
+
this.removeAttachment();
|
|
61458
|
+
this.detachedTargetId = request.targetId;
|
|
61459
|
+
this.options.disposeClient().catch((error) => this.options.onError(error));
|
|
61460
|
+
return;
|
|
61461
|
+
}
|
|
61462
|
+
if (this.attachment) {
|
|
61463
|
+
const previous = this.attachment;
|
|
61464
|
+
if ((await previous.connection.getTargets()).some((target) => target.targetId === previous.targetId)) throw new Error("A Stagehand client is already connected");
|
|
61465
|
+
await this.lost(previous);
|
|
61466
|
+
}
|
|
61467
|
+
const connection = await this.options.connection(request.cdpUrl);
|
|
61468
|
+
const attachment = {
|
|
61469
|
+
...request,
|
|
61470
|
+
connection,
|
|
61471
|
+
onDestroyed: ({ targetId }) => {
|
|
61472
|
+
if (targetId !== request.targetId) return;
|
|
61473
|
+
this.enqueue(() => this.lost(attachment)).catch((error) => this.options.onError(error));
|
|
61474
|
+
}
|
|
61475
|
+
};
|
|
61476
|
+
this.attachment = attachment;
|
|
61477
|
+
connection.on("Target.targetDestroyed", attachment.onDestroyed);
|
|
61478
|
+
try {
|
|
61479
|
+
const target = (await connection.getTargets()).find((target) => target.targetId === request.targetId);
|
|
61480
|
+
if (!target) {
|
|
61481
|
+
await this.lost(attachment);
|
|
61482
|
+
throw new Error("Stagehand client disconnected during registration");
|
|
61483
|
+
}
|
|
61484
|
+
if (target.url !== "about:blank#stagehand-client-connection") throw new Error("Invalid Stagehand client connection target");
|
|
61485
|
+
} catch (error) {
|
|
61486
|
+
if (this.attachment === attachment) this.removeAttachment();
|
|
61487
|
+
throw error;
|
|
61488
|
+
}
|
|
61489
|
+
});
|
|
61490
|
+
}
|
|
61491
|
+
async lost(attachment) {
|
|
61492
|
+
if (this.attachment !== attachment) return;
|
|
61493
|
+
this.removeAttachment();
|
|
61494
|
+
if (attachment.closeBrowserOnDisconnect) {
|
|
61495
|
+
try {
|
|
61496
|
+
await attachment.connection.send("Browser.close");
|
|
61497
|
+
} catch (error) {
|
|
61498
|
+
if (attachment.connection.connected) throw error;
|
|
61499
|
+
}
|
|
61500
|
+
return;
|
|
61501
|
+
}
|
|
61502
|
+
this.options.rejectClientCallbacks();
|
|
61503
|
+
try {
|
|
61504
|
+
await this.options.closeConnection();
|
|
61505
|
+
} finally {
|
|
61506
|
+
this.options.disposeClient().catch((error) => this.options.onError(error));
|
|
61507
|
+
}
|
|
61508
|
+
}
|
|
61509
|
+
removeAttachment() {
|
|
61510
|
+
const attachment = this.attachment;
|
|
61511
|
+
this.attachment = void 0;
|
|
61512
|
+
attachment?.connection.off("Target.targetDestroyed", attachment.onDestroyed);
|
|
61513
|
+
}
|
|
61514
|
+
enqueue(work) {
|
|
61515
|
+
const result = this.tail.then(work, work);
|
|
61516
|
+
this.tail = result.catch(() => void 0);
|
|
61517
|
+
return result;
|
|
61518
|
+
}
|
|
61519
|
+
};
|
|
61397
61520
|
//#endregion
|
|
61398
61521
|
//#region understudy/context.ts
|
|
61399
61522
|
function isMissingTargetError(error) {
|
|
@@ -61414,12 +61537,13 @@ function hasInjectableDOM(url) {
|
|
|
61414
61537
|
return false;
|
|
61415
61538
|
}
|
|
61416
61539
|
function isNonWebTarget(info) {
|
|
61540
|
+
if (info.url === "about:blank#stagehand-client-connection") return true;
|
|
61417
61541
|
if (info.type === "page") return false;
|
|
61418
61542
|
return info.type !== "iframe" || !hasInjectableDOM(info.url);
|
|
61419
61543
|
}
|
|
61420
61544
|
function isTopLevelPage(info) {
|
|
61421
61545
|
const ti = info;
|
|
61422
|
-
return info.type === "page" && ti.subtype !== "iframe";
|
|
61546
|
+
return info.type === "page" && ti.subtype !== "iframe" && info.url !== "about:blank#stagehand-client-connection";
|
|
61423
61547
|
}
|
|
61424
61548
|
var DEFAULT_ACTIVE_PAGE_TIMEOUT_MS = 3e3;
|
|
61425
61549
|
/**
|
|
@@ -62210,7 +62334,7 @@ var STAGEHAND_RUNTIME_VERSION = package_default.version;
|
|
|
62210
62334
|
function startStagehandServiceWorker(scope = globalThis, runtime) {
|
|
62211
62335
|
const chromeRuntimeClient = new ChromeRuntimeClient(scope, STAGEHAND_SEND_TO_HOST_BINDING);
|
|
62212
62336
|
let rpcClient;
|
|
62213
|
-
|
|
62337
|
+
const activeRuntime = runtime ?? createStagehandRuntime({
|
|
62214
62338
|
browserSessionFactory: async (cdpUrl, logger, bootstrapLogger) => {
|
|
62215
62339
|
const locatorRuntimeResponse = await fetch(chrome.runtime.getURL("content-script.js"));
|
|
62216
62340
|
if (!locatorRuntimeResponse.ok) throw new Error(`Failed to load Stagehand locator runtime: ${locatorRuntimeResponse.status}`);
|
|
@@ -62246,7 +62370,32 @@ function startStagehandServiceWorker(scope = globalThis, runtime) {
|
|
|
62246
62370
|
if (!rpcClient) throw new Error("Stagehand RPC client is not connected");
|
|
62247
62371
|
return await rpcClient.send(StagehandMethods.llmGenerate, params);
|
|
62248
62372
|
}
|
|
62249
|
-
})
|
|
62373
|
+
});
|
|
62374
|
+
rpcClient = new RPCClient(chromeRuntimeClient, new RPCRouter(activeRuntime));
|
|
62375
|
+
const lifecycle = new ClientLifecycle({
|
|
62376
|
+
connection: async (cdpUrl) => {
|
|
62377
|
+
await activeRuntime.ensureBrowserConnection(cdpUrl);
|
|
62378
|
+
const session = activeRuntime.browserSession;
|
|
62379
|
+
if (!(session instanceof BrowserContext)) throw new Error("Stagehand client lifecycle requires a CDP browser connection");
|
|
62380
|
+
return session.conn;
|
|
62381
|
+
},
|
|
62382
|
+
rejectClientCallbacks: () => rpcClient?.releaseClient(),
|
|
62383
|
+
closeConnection: () => activeRuntime.dropBrowserConnection(),
|
|
62384
|
+
disposeClient: () => activeRuntime.disposeStagehandInstance(),
|
|
62385
|
+
onError: (error) => {
|
|
62386
|
+
console.error("Stagehand client lifecycle cleanup failed", error);
|
|
62387
|
+
}
|
|
62388
|
+
});
|
|
62389
|
+
scope.__stagehandClientLifecycle = async (input) => {
|
|
62390
|
+
const action = typeof input === "object" && input !== null && "action" in input ? input.action : void 0;
|
|
62391
|
+
try {
|
|
62392
|
+
await lifecycle.handle(input);
|
|
62393
|
+
} catch (error) {
|
|
62394
|
+
if (action === "detach") rpcClient?.acceptClient();
|
|
62395
|
+
throw error;
|
|
62396
|
+
}
|
|
62397
|
+
if (action === "attach") rpcClient?.acceptClient();
|
|
62398
|
+
};
|
|
62250
62399
|
scope.__stagehand_runtime = {
|
|
62251
62400
|
protocolVersion: STAGEHAND_PROTOCOL_VERSION,
|
|
62252
62401
|
serverInfo: {
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/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-d59afde37e995b165a41f754a12a446f11d471a2'
|
|
@@ -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.dev1660}/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.dev1660}/src/stagehand/_generated/__init__.py
RENAMED
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/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.dev1660}/src/stagehand/browserbase_connection.py
RENAMED
|
File without changes
|
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browserbase_services.py
RENAMED
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/src/stagehand/browserbase_session.py
RENAMED
|
File without changes
|
{stagehand-4.3.1a0.dev1658 → stagehand-4.4.0a0.dev1660}/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
|