stagehand 4.0.3a0.dev1474__tar.gz → 4.0.3a0.dev1476__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.
Files changed (35) hide show
  1. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/PKG-INFO +1 -1
  2. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/pyproject.toml +1 -1
  3. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/service-worker.js +140 -68
  4. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_generated/input_types.py +0 -4
  5. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_generated/models.py +0 -8
  6. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_generated/protocol_version.py +1 -1
  7. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/browser.py +56 -13
  8. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/browser_context.py +11 -9
  9. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/browserbase_session.py +18 -1
  10. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/rpc_client.py +0 -1
  11. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/stagehand.py +15 -5
  12. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/README.md +0 -0
  13. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/__init__.py +0 -0
  14. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/blank.html +0 -0
  15. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/content-script.js +0 -0
  16. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/manifest.json +0 -0
  17. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
  18. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
  19. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/wake-service-worker.html +0 -0
  20. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_extension/wake-service-worker.js +0 -0
  21. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_generated/__init__.py +0 -0
  22. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_sdk_identity.py +0 -0
  23. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/_validation.py +0 -0
  24. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/browser_clipboard.py +0 -0
  25. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/cdp_client.py +0 -0
  26. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/client_models.py +0 -0
  27. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/client_types.py +0 -0
  28. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/extension_assets.py +0 -0
  29. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/file_upload.py +0 -0
  30. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/locator.py +0 -0
  31. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/page.py +0 -0
  32. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/py.typed +0 -0
  33. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/response.py +0 -0
  34. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/timeouts.py +0 -0
  35. {stagehand-4.0.3a0.dev1474 → stagehand-4.0.3a0.dev1476}/src/stagehand/webmcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stagehand
3
- Version: 4.0.3a0.dev1474
3
+ Version: 4.0.3a0.dev1476
4
4
  Summary: Browserbase's SDK for building browser agents
5
5
  Keywords: ai,browser,automation,web-scraping,testing
6
6
  Author: Browserbase
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stagehand"
3
- version = "4.0.3a0.dev1474"
3
+ version = "4.0.3a0.dev1476"
4
4
  description = "Browserbase's SDK for building browser agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -7359,7 +7359,7 @@ function isRecord$1(value) {
7359
7359
  }
7360
7360
  var package_default$1 = {
7361
7361
  name: "@browserbasehq/stagehand-protocol",
7362
- version: "2.0.0-alpha-d49d643ce9311f16b3ab21289c3bcc01bf0eb097",
7362
+ version: "2.0.0-alpha-6555e81e837012088eb12a39c06aed0527a2e5af",
7363
7363
  "private": true,
7364
7364
  type: "module",
7365
7365
  scripts: {
@@ -8302,7 +8302,6 @@ var PageNavigationOptionsSchema = strictObject({
8302
8302
  }).meta({ id: "PageNavigationOptions" });
8303
8303
  var PageVoidResultSchema = strictObject({ ok: literal(true) }).meta({ id: "PageVoidResult" });
8304
8304
  var ContextVoidResultSchema = strictObject({ ok: literal(true) }).meta({ id: "ContextVoidResult" });
8305
- var ContextCloseResultSchema = strictObject({ closed: literal(true) }).meta({ id: "ContextCloseResult" });
8306
8305
  var PageScreenshotClipSchema = strictObject({
8307
8306
  x: number$1(),
8308
8307
  y: number$1(),
@@ -8784,11 +8783,6 @@ var StagehandMethods = {
8784
8783
  params: ContextSetActivePageParamsSchema,
8785
8784
  result: ContextVoidResultSchema
8786
8785
  },
8787
- contextClose: {
8788
- name: "context.close",
8789
- params: EmptyParamsSchema,
8790
- result: ContextCloseResultSchema
8791
- },
8792
8786
  contextAddInitScript: {
8793
8787
  name: "context.add_init_script",
8794
8788
  params: ContextAddInitScriptParamsSchema,
@@ -11460,7 +11454,9 @@ var RPCClient = class {
11460
11454
  });
11461
11455
  return;
11462
11456
  }
11457
+ let releaseStagehandInstanceRequest;
11463
11458
  try {
11459
+ if (request.data.method !== StagehandMethods.stagehandInit.name && request.data.method !== StagehandMethods.stagehandClose.name) releaseStagehandInstanceRequest = this.router.runtime.acquireStagehandInstanceRequest();
11464
11460
  const result = await this.router.handle(stagehandRequest.data, runtimeAttachments);
11465
11461
  const parsedResult = method.result.safeParse(result);
11466
11462
  if (!parsedResult.success) {
@@ -11470,6 +11466,7 @@ var RPCClient = class {
11470
11466
  });
11471
11467
  return;
11472
11468
  }
11469
+ await this.router.beforeResponse(stagehandRequest.data);
11473
11470
  await this.runtime.send(JSONRPCSuccessResponseSchema.parse({
11474
11471
  jsonrpc: "2.0",
11475
11472
  id: request.data.id,
@@ -11484,6 +11481,8 @@ var RPCClient = class {
11484
11481
  return;
11485
11482
  }
11486
11483
  await this.sendError(request.data.id, JSONRPCErrorCodes.internalError, error instanceof Error ? error.message : String(error), { name: error instanceof Error ? error.name : "Error" });
11484
+ } finally {
11485
+ releaseStagehandInstanceRequest?.();
11487
11486
  }
11488
11487
  }
11489
11488
  receiveResponse(response) {
@@ -11550,10 +11549,6 @@ function createContextController(runtime) {
11550
11549
  logger.debug("context.set_active_page", {});
11551
11550
  return runtime.contextSetActivePage(params);
11552
11551
  }
11553
- async function close(_params, { logger }) {
11554
- logger.debug("context.close", {});
11555
- return runtime.contextClose();
11556
- }
11557
11552
  async function addInitScript(params, { logger }) {
11558
11553
  logger.debug("context.add_init_script", {});
11559
11554
  return runtime.contextAddInitScript(params);
@@ -11611,7 +11606,6 @@ function createContextController(runtime) {
11611
11606
  newPage,
11612
11607
  activePage,
11613
11608
  setActivePage,
11614
- close,
11615
11609
  addInitScript,
11616
11610
  setExtraHTTPHeaders,
11617
11611
  getDomainPolicy,
@@ -12330,8 +12324,9 @@ function reportPageEventListenerError(error) {
12330
12324
  //#endregion
12331
12325
  //#region ../sdk-ts/src/browserContext.ts
12332
12326
  var BrowserContext$1 = class {
12333
- constructor(rpcClient) {
12327
+ constructor(rpcClient, closeBrowser) {
12334
12328
  this.rpcClient = rpcClient;
12329
+ this.closeBrowser = closeBrowser;
12335
12330
  }
12336
12331
  get clipboard() {
12337
12332
  return this.clipboardRef ??= new BrowserClipboard(this.rpcClient);
@@ -12351,9 +12346,10 @@ var BrowserContext$1 = class {
12351
12346
  async setActivePage(page) {
12352
12347
  await this.rpcClient.send(StagehandMethods.contextSetActivePage, { pageId: page.pageId });
12353
12348
  }
12354
- /** Close the remote context. Call Stagehand.close() to dispose the SDK's local resources. */
12349
+ /** Close the underlying browser. */
12355
12350
  async close() {
12356
- await this.rpcClient.send(StagehandMethods.contextClose, {});
12351
+ if (!this.closeBrowser) throw new Error("Browser context is not attached to a browser");
12352
+ await this.closeBrowser();
12357
12353
  }
12358
12354
  async addInitScript(script, arg) {
12359
12355
  const source = await normalizeInitScriptSource$1(script, arg, "context.addInitScript");
@@ -50846,13 +50842,13 @@ async function observe({ params, page, model, clientLLMGenerate, logger, systemP
50846
50842
  //#endregion
50847
50843
  //#region controllers/stagehandController.ts
50848
50844
  function createStagehandController(runtime, options = {}) {
50849
- const closeRuntime = options.close ?? (() => runtime.close());
50845
+ const closeRuntime = options.close ?? (() => runtime.disposeStagehandInstance());
50850
50846
  async function runOperation(name, { logger, telemetryScope }, run) {
50851
50847
  return await logger.span(name, {}, (logger) => runtime.runWithTelemetryContext(telemetryScope, logger, () => run(logger)));
50852
50848
  }
50853
50849
  async function init(params, { logger }) {
50854
50850
  const compatibility = checkProtocolCompatibility(params.protocolVersion, STAGEHAND_PROTOCOL_VERSION);
50855
- if (!compatibility.compatible) throw new StagehandProtocolCompatibilityError(compatibility.reason);
50851
+ if (compatibility.compatible === false) throw new StagehandProtocolCompatibilityError(compatibility.reason);
50856
50852
  logger.setLevel(params.logLevel);
50857
50853
  logger.info("stagehand.init", {});
50858
50854
  return options.initialize ? await options.initialize(params) : await runtime.initialize(params, logger);
@@ -50961,7 +50957,7 @@ var RPCRouter = class {
50961
50957
  }
50962
50958
  async handle(request, runtimeAttachments) {
50963
50959
  const initParams = request.method === StagehandMethods.stagehandInit.name ? request.params : void 0;
50964
- if (initParams) this.runtime.tracing.configure(initParams.telemetry, initParams.clientInfo);
50960
+ if (initParams) await this.runtime.tracing.configure(initParams.telemetry, initParams.clientInfo);
50965
50961
  const parentContext = W3C_TRACE_CONTEXT_PROPAGATOR.extract(ROOT_CONTEXT, request, {
50966
50962
  get(carrier, key) {
50967
50963
  if (key === "traceparent" || key === "tracestate") return carrier[key];
@@ -50996,9 +50992,12 @@ var RPCRouter = class {
50996
50992
  throw error;
50997
50993
  } finally {
50998
50994
  span.end();
50999
- if (request.method === StagehandMethods.stagehandClose.name) await this.runtime.tracing.shutdown();
51000
50995
  }
51001
50996
  }
50997
+ async beforeResponse(request) {
50998
+ if (request.method !== StagehandMethods.stagehandClose.name) return;
50999
+ await this.runtime.tracing.forceFlush().catch(() => void 0);
51000
+ }
51002
51001
  async route(request, context, parsedInitParams) {
51003
51002
  switch (request.method) {
51004
51003
  case "stagehand.init": return this.stagehandController.init(parsedInitParams ?? parseParams(StagehandMethods.stagehandInit, request.params), context);
@@ -51012,7 +51011,6 @@ var RPCRouter = class {
51012
51011
  case "context.new_page": return this.contextController.newPage(parseParams(StagehandMethods.contextNewPage, request.params), context);
51013
51012
  case "context.active_page": return this.contextController.activePage(parseParams(StagehandMethods.contextActivePage, request.params), context);
51014
51013
  case "context.set_active_page": return this.contextController.setActivePage(parseParams(StagehandMethods.contextSetActivePage, request.params), context);
51015
- case "context.close": return this.contextController.close(parseParams(StagehandMethods.contextClose, request.params), context);
51016
51014
  case "context.add_init_script": return this.contextController.addInitScript(parseParams(StagehandMethods.contextAddInitScript, request.params), context);
51017
51015
  case "context.set_extra_http_headers": return this.contextController.setExtraHTTPHeaders(parseParams(StagehandMethods.contextSetExtraHTTPHeaders, request.params), context);
51018
51016
  case "context.get_domain_policy": return this.contextController.getDomainPolicy(parseParams(StagehandMethods.contextGetDomainPolicy, request.params), context);
@@ -51260,14 +51258,10 @@ var StagehandLogger = class StagehandLogger {
51260
51258
  };
51261
51259
  //#endregion
51262
51260
  //#region runtimeState.ts
51263
- var StagehandRuntimeStateSchema = discriminatedUnion("status", [
51264
- strictObject({ status: literal("created") }),
51265
- strictObject({
51266
- status: literal("initialized"),
51267
- initParams: StagehandInitParamsSchema
51268
- }),
51269
- strictObject({ status: literal("closed") })
51270
- ]);
51261
+ var StagehandRuntimeStateSchema = discriminatedUnion("status", [strictObject({ status: literal("idle") }), strictObject({
51262
+ status: literal("initialized"),
51263
+ initParams: StagehandInitParamsSchema
51264
+ })]);
51271
51265
  //#endregion
51272
51266
  //#region ../../node_modules/.pnpm/@opentelemetry+otlp-exporter-base@0.220.0_@opentelemetry+api@1.9.1/node_modules/@opentelemetry/otlp-exporter-base/build/esm/OTLPExporterBase.js
51273
51267
  var OTLPExporterBase = class {
@@ -53443,7 +53437,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
53443
53437
  };
53444
53438
  var package_default = {
53445
53439
  name: "@browserbasehq/stagehand-extension",
53446
- version: "1.0.2-alpha-d49d643ce9311f16b3ab21289c3bcc01bf0eb097",
53440
+ version: "1.0.2-alpha-6555e81e837012088eb12a39c06aed0527a2e5af",
53447
53441
  "private": true,
53448
53442
  type: "module",
53449
53443
  scripts: {
@@ -53518,22 +53512,48 @@ function createStagehandTracing(options = {}, dependencies = DEFAULT_TRACING_RUN
53518
53512
  const pendingTracer = trace.getTracer(STAGEHAND_TRACER_NAME);
53519
53513
  let runtime;
53520
53514
  let shutDown = false;
53515
+ let globalsRegistered = false;
53516
+ let lifecycleTail = Promise.resolve();
53517
+ let activeTelemetry;
53518
+ let activeClientInfo;
53519
+ function enqueueLifecycle(run) {
53520
+ const result = lifecycleTail.then(run, run);
53521
+ lifecycleTail = result.catch(() => void 0);
53522
+ return result;
53523
+ }
53521
53524
  return {
53522
53525
  get tracer() {
53523
53526
  return runtime?.tracer ?? pendingTracer;
53524
53527
  },
53525
53528
  configure(telemetry, clientInfo) {
53526
- if (runtime || shutDown) return;
53527
- runtime = createStagehandTracingRuntime({
53528
- ...options,
53529
- clientName: clientInfo.name,
53530
- clientVersion: clientInfo.version
53531
- }, { spanProcessors: [...dependencies.spanProcessors, createOtlpSpanProcessor(telemetry.traces)] });
53529
+ return enqueueLifecycle(async () => {
53530
+ if (shutDown) return;
53531
+ if (runtime && telemetry === activeTelemetry && clientInfo === activeClientInfo) return;
53532
+ const previousRuntime = runtime;
53533
+ runtime = void 0;
53534
+ await previousRuntime?.shutdown();
53535
+ const registerGlobals = options.registerGlobals !== false && !globalsRegistered;
53536
+ runtime = createStagehandTracingRuntime({
53537
+ ...options,
53538
+ clientName: clientInfo.name,
53539
+ clientVersion: clientInfo.version,
53540
+ registerGlobals
53541
+ }, { spanProcessors: [...dependencies.spanProcessors, createOtlpSpanProcessor(telemetry.traces)] });
53542
+ activeTelemetry = telemetry;
53543
+ activeClientInfo = clientInfo;
53544
+ globalsRegistered ||= registerGlobals;
53545
+ });
53532
53546
  },
53533
- forceFlush: () => runtime?.forceFlush() ?? Promise.resolve(),
53547
+ forceFlush: () => enqueueLifecycle(() => runtime?.forceFlush() ?? Promise.resolve()),
53534
53548
  shutdown: () => {
53535
53549
  shutDown = true;
53536
- return runtime?.shutdown() ?? Promise.resolve();
53550
+ return enqueueLifecycle(async () => {
53551
+ const activeRuntime = runtime;
53552
+ runtime = void 0;
53553
+ activeTelemetry = void 0;
53554
+ activeClientInfo = void 0;
53555
+ await activeRuntime?.shutdown();
53556
+ });
53537
53557
  }
53538
53558
  };
53539
53559
  }
@@ -56822,6 +56842,9 @@ var StagehandMetricsAccumulator = class {
56822
56842
  snapshot() {
56823
56843
  return { ...this.values };
56824
56844
  }
56845
+ reset() {
56846
+ Object.assign(this.values, EMPTY_METRICS);
56847
+ }
56825
56848
  };
56826
56849
  //#endregion
56827
56850
  //#region responseHandleTable.ts
@@ -56909,10 +56932,13 @@ var StagehandRuntime = class {
56909
56932
  this.tracing = tracing;
56910
56933
  this.metrics = new StagehandMetricsAccumulator();
56911
56934
  this.responseHandles = new ResponseHandleTable();
56912
- this.state = createStore()(() => StagehandRuntimeStateSchema.parse({ status: "created" }));
56935
+ this.state = createStore()(() => StagehandRuntimeStateSchema.parse({ status: "idle" }));
56913
56936
  this.pagesById = /* @__PURE__ */ new Map();
56914
56937
  this.pageEventSubscriptions = /* @__PURE__ */ new Map();
56915
56938
  this.initializationInProgress = false;
56939
+ this.lifecycleTail = Promise.resolve();
56940
+ this.stagehandInstanceClosing = false;
56941
+ this.activeStagehandInstanceRequests = 0;
56916
56942
  this.logger = new StagehandLogger(tracing, adapters.emitLog);
56917
56943
  }
56918
56944
  async replaceBrowserConnection(params, bootstrapLogger) {
@@ -56932,29 +56958,31 @@ var StagehandRuntime = class {
56932
56958
  }
56933
56959
  }
56934
56960
  async initialize(params, logger = this.logger) {
56935
- const state = this.state.getState();
56936
- if (state.status === "closed") throw new Error("Stagehand has been closed and cannot be initialized again");
56937
56961
  if (this.initializationInProgress) throw new Error("Stagehand initialization is already in progress");
56938
56962
  this.initializationInProgress = true;
56939
56963
  try {
56940
- this.logger.setLevel(params.logLevel);
56941
- if (!this.browserSession) {
56942
- if (!params.browserCdpUrl) throw new Error("stagehand.init requires browserCdpUrl until resident mode is active");
56943
- await this.replaceBrowserConnection({ cdpUrl: params.browserCdpUrl }, logger);
56944
- }
56945
- const pages = await this.runWithTelemetryContext(Symbol("stagehand.init"), logger, async () => {
56946
- if (state.status === "created") await this.browserSession?.prepareForInitialization?.();
56947
- return await this.contextPages();
56948
- });
56949
- this.tracing.configure(params.telemetry, params.clientInfo);
56950
- this.state.setState(StagehandRuntimeStateSchema.parse({
56951
- status: "initialized",
56952
- initParams: params
56953
- }), true);
56954
- return {
56955
- initialized: true,
56956
- pages
56957
- };
56964
+ return await this.enqueueLifecycle(async () => {
56965
+ const state = this.state.getState();
56966
+ if (state.status !== "idle") throw new Error("A Stagehand instance is already initialized");
56967
+ this.logger.setLevel(params.logLevel);
56968
+ if (!this.browserSession?.connected) {
56969
+ if (!params.browserCdpUrl) throw new Error("stagehand.init requires browserCdpUrl until resident mode is active");
56970
+ await this.replaceBrowserConnection({ cdpUrl: params.browserCdpUrl }, logger);
56971
+ }
56972
+ const pages = await this.runWithTelemetryContext(Symbol("stagehand.init"), logger, async () => {
56973
+ if (state.status === "idle") await this.browserSession?.prepareForInitialization?.();
56974
+ return await this.contextPages();
56975
+ });
56976
+ await this.tracing.configure(params.telemetry, params.clientInfo);
56977
+ this.state.setState(StagehandRuntimeStateSchema.parse({
56978
+ status: "initialized",
56979
+ initParams: params
56980
+ }), true);
56981
+ return {
56982
+ initialized: true,
56983
+ pages
56984
+ };
56985
+ });
56958
56986
  } finally {
56959
56987
  this.initializationInProgress = false;
56960
56988
  }
@@ -56992,10 +57020,6 @@ var StagehandRuntime = class {
56992
57020
  await this.requireBrowserSession().setActivePage(page);
56993
57021
  return { ok: true };
56994
57022
  }
56995
- async contextClose() {
56996
- await this.close();
56997
- return { closed: true };
56998
- }
56999
57023
  async contextAddInitScript(params) {
57000
57024
  await this.requireBrowserSession().addInitScript(params.source);
57001
57025
  return { ok: true };
@@ -57291,16 +57315,64 @@ var StagehandRuntime = class {
57291
57315
  return { set: true };
57292
57316
  }
57293
57317
  async close() {
57294
- const session = this.browserSession;
57295
- this.browserSession = void 0;
57318
+ await this.enqueueLifecycle(async () => {
57319
+ const session = this.browserSession;
57320
+ this.browserSession = void 0;
57321
+ this.clearStagehandInstance();
57322
+ await session?.close();
57323
+ });
57324
+ }
57325
+ async disposeStagehandInstance() {
57326
+ if (this.stagehandInstanceDisposal) return await this.stagehandInstanceDisposal;
57327
+ this.stagehandInstanceClosing = true;
57328
+ const disposal = this.enqueueLifecycle(async () => {
57329
+ await this.waitForStagehandInstanceRequests();
57330
+ this.clearStagehandInstance();
57331
+ });
57332
+ this.stagehandInstanceDisposal = disposal.finally(() => {
57333
+ this.stagehandInstanceClosing = false;
57334
+ this.stagehandInstanceDisposal = void 0;
57335
+ });
57336
+ return await this.stagehandInstanceDisposal;
57337
+ }
57338
+ acquireStagehandInstanceRequest() {
57339
+ if (this.stagehandInstanceClosing) throw new Error("Stagehand instance is closing");
57340
+ this.activeStagehandInstanceRequests += 1;
57341
+ let released = false;
57342
+ return () => {
57343
+ if (released) return;
57344
+ released = true;
57345
+ this.activeStagehandInstanceRequests -= 1;
57346
+ if (this.activeStagehandInstanceRequests !== 0) return;
57347
+ this.stagehandInstanceRequestsDrained?.resolve();
57348
+ this.stagehandInstanceRequestsDrained = void 0;
57349
+ };
57350
+ }
57351
+ clearStagehandInstance() {
57296
57352
  this.disposeAllPageEventSubscriptions();
57297
57353
  this.pagesById.clear();
57298
57354
  this.responseHandles.clear();
57299
- try {
57300
- await session?.close();
57301
- } finally {
57302
- this.state.setState(StagehandRuntimeStateSchema.parse({ status: "closed" }), true);
57355
+ this.metrics.reset();
57356
+ this.state.setState(StagehandRuntimeStateSchema.parse({ status: "idle" }), true);
57357
+ }
57358
+ enqueueLifecycle(run) {
57359
+ const result = this.lifecycleTail.then(run, run);
57360
+ this.lifecycleTail = result.then(() => void 0, () => void 0);
57361
+ return result;
57362
+ }
57363
+ waitForStagehandInstanceRequests() {
57364
+ if (this.activeStagehandInstanceRequests === 0) return Promise.resolve();
57365
+ if (!this.stagehandInstanceRequestsDrained) {
57366
+ let resolve;
57367
+ const promise = new Promise((drained) => {
57368
+ resolve = drained;
57369
+ });
57370
+ this.stagehandInstanceRequestsDrained = {
57371
+ promise,
57372
+ resolve
57373
+ };
57303
57374
  }
57375
+ return this.stagehandInstanceRequestsDrained.promise;
57304
57376
  }
57305
57377
  pageRefForId(pageId) {
57306
57378
  return pageRefFromUnderstudyPage(this.resolvePage(pageId));
@@ -140,10 +140,6 @@ class ContextClipboardWriteTextParams(TypedDict):
140
140
  text: str
141
141
 
142
142
 
143
- class ContextCloseResult(TypedDict):
144
- closed: Literal[True]
145
-
146
-
147
143
  class ContextCookiesParams(TypedDict):
148
144
  urls: NotRequired[str | list[str]]
149
145
 
@@ -430,14 +430,6 @@ class ContextClipboardWriteTextParams(WireModel):
430
430
  text: StrictStr
431
431
 
432
432
 
433
- class ContextCloseResult(WireModel):
434
- model_config = ConfigDict(
435
- extra="forbid",
436
- validate_by_name=True,
437
- )
438
- closed: Literal[True]
439
-
440
-
441
433
  class ContextCookiesParams(WireModel):
442
434
  model_config = ConfigDict(
443
435
  extra="forbid",
@@ -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 = '2.0.0-alpha-d49d643ce9311f16b3ab21289c3bcc01bf0eb097'
4
+ STAGEHAND_PROTOCOL_VERSION = '2.0.0-alpha-6555e81e837012088eb12a39c06aed0527a2e5af'
@@ -22,7 +22,7 @@ from ._generated.models import (
22
22
  BrowserSessionMetadata,
23
23
  )
24
24
  from .browserbase_session import DEFAULT_BROWSERBASE_URL, _create_browserbase_session_client
25
- from .cdp_client import CDPClient
25
+ from .cdp_client import CDPClient, CDPConnectionClosedError
26
26
  from .client_models import (
27
27
  BrowserbaseConnectOptions,
28
28
  LocalBrowserConnectOptions,
@@ -98,7 +98,8 @@ class StagehandBrowser:
98
98
  "_attachment",
99
99
  "_claimed",
100
100
  "_close_callback",
101
- "_close_task",
101
+ "_invalidate_callback",
102
+ "_terminal_task",
102
103
  "_context",
103
104
  "_origin",
104
105
  "_provider",
@@ -112,6 +113,7 @@ class StagehandBrowser:
112
113
  attachment: _ClaimedBrowser,
113
114
  close: Callable[[], Awaitable[None]],
114
115
  *,
116
+ invalidate: Callable[[], Awaitable[None]] | None = None,
115
117
  session_id: str | None = None,
116
118
  _token: object | None = None,
117
119
  ) -> None:
@@ -121,8 +123,9 @@ class StagehandBrowser:
121
123
  self._origin = origin
122
124
  self._attachment = attachment
123
125
  self._close_callback = close
126
+ self._invalidate_callback = invalidate or close
124
127
  self._claimed = False
125
- self._close_task: asyncio.Task[None] | None = None
128
+ self._terminal_task: asyncio.Task[None] | None = None
126
129
  self._context: BrowserContext | None = None
127
130
  self._session_id = session_id
128
131
 
@@ -141,7 +144,7 @@ class StagehandBrowser:
141
144
 
142
145
  @property
143
146
  def closed(self) -> bool:
144
- return self._close_task is not None
147
+ return self._terminal_task is not None
145
148
 
146
149
  @property
147
150
  def context(self) -> BrowserContext:
@@ -153,15 +156,12 @@ class StagehandBrowser:
153
156
  return self._context
154
157
 
155
158
  def close(self) -> Awaitable[None]:
156
- if self._close_task is None:
157
- self._close_task = asyncio.create_task(
158
- self._run_close(),
159
+ if self._terminal_task is None:
160
+ self._terminal_task = asyncio.create_task(
161
+ _run_browser_terminal_callback(self._close_callback),
159
162
  name="stagehand-browser-close",
160
163
  )
161
- return asyncio.shield(self._close_task)
162
-
163
- async def _run_close(self) -> None:
164
- await self._close_callback()
164
+ return asyncio.shield(self._terminal_task)
165
165
 
166
166
 
167
167
  def _claim_browser(browser: object) -> _ClaimedBrowser:
@@ -181,6 +181,23 @@ def _release_browser(browser: StagehandBrowser) -> None:
181
181
  browser._claimed = False
182
182
 
183
183
 
184
+ def _invalidate_browser(browser: StagehandBrowser) -> Awaitable[None]:
185
+ if not isinstance(browser, StagehandBrowser):
186
+ raise TypeError("browser must be created by local_browser or browserbase")
187
+ if browser._terminal_task is None:
188
+ browser._terminal_task = asyncio.create_task(
189
+ _run_browser_terminal_callback(browser._invalidate_callback),
190
+ name="stagehand-browser-invalidate",
191
+ )
192
+ return asyncio.shield(browser._terminal_task)
193
+
194
+
195
+ async def _run_browser_terminal_callback(
196
+ callback: Callable[[], Awaitable[None]],
197
+ ) -> None:
198
+ await callback()
199
+
200
+
184
201
  def _attach_browser_context(
185
202
  browser: StagehandBrowser,
186
203
  context: BrowserContext,
@@ -289,13 +306,34 @@ async def _connect_browser(
289
306
 
290
307
  connected_client = cdp_client
291
308
 
292
- async def close() -> None:
309
+ async def invalidate() -> None:
293
310
  try:
294
311
  await connected_client.close()
295
312
  finally:
296
313
  if owns_source:
297
314
  await source.close()
298
315
 
316
+ async def close() -> None:
317
+ errors: list[BaseException] = []
318
+ try:
319
+ if provider == "local" and origin == "connected":
320
+ try:
321
+ await connected_client.send_command("Browser.close")
322
+ except CDPConnectionClosedError:
323
+ pass
324
+ else:
325
+ await source.close()
326
+ except BaseException as error:
327
+ errors.append(error)
328
+ try:
329
+ await connected_client.close()
330
+ except BaseException as error:
331
+ errors.append(error)
332
+ if len(errors) == 1:
333
+ raise errors[0]
334
+ if errors:
335
+ raise BaseExceptionGroup("Browser termination and cleanup failed", errors)
336
+
299
337
  return StagehandBrowser(
300
338
  provider,
301
339
  origin,
@@ -304,6 +342,7 @@ async def _connect_browser(
304
342
  worker_init_metadata=worker_init_metadata,
305
343
  ),
306
344
  close,
345
+ invalidate=invalidate,
307
346
  session_id=(
308
347
  worker_init_metadata.browser.session_id if worker_init_metadata.browser else None
309
348
  ),
@@ -563,7 +602,11 @@ class BrowserbaseBrowser:
563
602
  return await _connect_browser(
564
603
  provider="browserbase",
565
604
  origin="connected",
566
- source=_ConnectedBrowserSource(connection.cdp_url),
605
+ source=ResolvedBrowserSource(
606
+ cdp_url=connection.cdp_url,
607
+ keep_alive=True,
608
+ _close_callback=connection.close,
609
+ ),
567
610
  extension_id=options.extension_id,
568
611
  preloaded_extension=options.extension_id is None,
569
612
  worker_init_metadata=_WorkerInitMetadata(
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from collections.abc import Mapping, Sequence
4
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
5
5
  from pathlib import Path
6
6
 
7
7
  from ._generated.input_types import CookieParam
@@ -12,7 +12,6 @@ from ._generated.models import (
12
12
  ContextAddCookiesParams,
13
13
  ContextAddInitScriptParams,
14
14
  ContextClearCookiesParams,
15
- ContextCloseResult,
16
15
  ContextCookiesParams,
17
16
  ContextCookiesResult,
18
17
  ContextGetDomainPolicyResult,
@@ -35,8 +34,13 @@ from .rpc_client import RPCClient
35
34
 
36
35
 
37
36
  class BrowserContext:
38
- def __init__(self, rpc_client: RPCClient) -> None:
37
+ def __init__(
38
+ self,
39
+ rpc_client: RPCClient,
40
+ close_browser: Callable[[], Awaitable[None]] | None = None,
41
+ ) -> None:
39
42
  self._rpc_client = rpc_client
43
+ self._close_browser = close_browser
40
44
  self._clipboard: BrowserClipboard | None = None
41
45
 
42
46
  @property
@@ -76,12 +80,10 @@ class BrowserContext:
76
80
  )
77
81
 
78
82
  async def close(self) -> None:
79
- """Close the remote context; use Stagehand.close() to release local resources."""
80
- await self._rpc_client.send(
81
- "context.close",
82
- EmptyParams(),
83
- ContextCloseResult,
84
- )
83
+ """Close the underlying browser."""
84
+ if self._close_browser is None:
85
+ raise RuntimeError("Browser context is not attached to a browser")
86
+ await self._close_browser()
85
87
 
86
88
  async def add_init_script(self, source: str | Path) -> None:
87
89
  if isinstance(source, Path):
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- from collections.abc import Mapping
4
+ from collections.abc import Awaitable, Callable, Mapping
5
5
  from dataclasses import dataclass, field
6
6
  from typing import Any, Protocol
7
7
 
@@ -142,6 +142,11 @@ class _BrowserbaseSessionConnection:
142
142
  session_id: str
143
143
  cdp_url: str
144
144
  region: BrowserbaseRegion | None = None
145
+ _close_callback: Callable[[], Awaitable[None]] | None = field(default=None, repr=False)
146
+
147
+ async def close(self) -> None:
148
+ if self._close_callback is not None:
149
+ await self._close_callback()
145
150
 
146
151
 
147
152
  @dataclass
@@ -254,10 +259,22 @@ class _BrowserbaseSessionClient:
254
259
  cdp_url = connect_url.strip() if connect_url is not None else ""
255
260
  if not cdp_url:
256
261
  raise BrowserbaseSessionError("Browserbase session is not available for connection")
262
+ released = False
263
+ close_lock = asyncio.Lock()
264
+
265
+ async def close() -> None:
266
+ nonlocal released
267
+ async with close_lock:
268
+ if released:
269
+ return
270
+ await self._api.release_session(retrieved_id.strip() or normalized_session_id)
271
+ released = True
272
+
257
273
  return _BrowserbaseSessionConnection(
258
274
  session_id=retrieved_id.strip() or normalized_session_id,
259
275
  cdp_url=cdp_url,
260
276
  region=region,
277
+ _close_callback=close,
261
278
  )
262
279
 
263
280
  async def _delete_extension_best_effort(self, extension_id: str | None) -> None:
@@ -38,7 +38,6 @@ _UNBOUNDED_BY_DEFAULT_METHODS = {
38
38
  "stagehand.extract",
39
39
  "stagehand.observe",
40
40
  "context.new_page",
41
- "context.close",
42
41
  "context.add_init_script",
43
42
  "context.set_extra_http_headers",
44
43
  "context.get_domain_policy",
@@ -47,6 +47,7 @@ from .browser import (
47
47
  _claim_browser,
48
48
  _ClaimedBrowser,
49
49
  _detach_browser_context,
50
+ _invalidate_browser,
50
51
  _release_browser,
51
52
  )
52
53
  from .browser_context import BrowserContext
@@ -278,7 +279,10 @@ class Stagehand:
278
279
  init_params,
279
280
  StagehandInitResult,
280
281
  )
281
- _attach_browser_context(self._browser_handle, BrowserContext(rpc_client))
282
+ _attach_browser_context(
283
+ self._browser_handle,
284
+ BrowserContext(rpc_client, self._browser_handle.close),
285
+ )
282
286
  self._initialized = True
283
287
 
284
288
  async def act(
@@ -450,6 +454,7 @@ class Stagehand:
450
454
 
451
455
  async def close(self) -> None:
452
456
  async def close_impl() -> None:
457
+ release_browser_claim = not self._initialized
453
458
  try:
454
459
  if self._initialized and self._rpc_client is not None:
455
460
  try:
@@ -458,10 +463,15 @@ class Stagehand:
458
463
  EmptyParams(),
459
464
  StagehandCloseResult,
460
465
  )
466
+ release_browser_claim = True
461
467
  except CDPConnectionClosedError:
462
- pass
468
+ release_browser_claim = True
463
469
  finally:
464
- await asyncio.shield(self._release_resources())
470
+ try:
471
+ await asyncio.shield(self._release_resources())
472
+ finally:
473
+ if release_browser_claim:
474
+ _release_browser(self._browser_handle)
465
475
 
466
476
  if self._close_task is None:
467
477
  self._close_task = asyncio.create_task(
@@ -541,11 +551,11 @@ class Stagehand:
541
551
  fail_closed: bool,
542
552
  ) -> None:
543
553
  if fail_closed:
544
- browser_close = browser.close()
554
+ browser_invalidation = _invalidate_browser(browser)
545
555
  try:
546
556
  await self._release_resources()
547
557
  finally:
548
- await browser_close
558
+ await browser_invalidation
549
559
  return
550
560
 
551
561
  try: