stagehand 4.2.0a0.dev1541__tar.gz → 4.2.0a0.dev1544__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 (36) hide show
  1. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/PKG-INFO +1 -1
  2. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/pyproject.toml +1 -1
  3. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/__init__.py +2 -0
  4. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/service-worker.js +91 -19
  5. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_generated/input_types.py +33 -0
  6. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_generated/models.py +55 -0
  7. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_generated/protocol_version.py +1 -1
  8. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/page.py +52 -0
  9. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/rpc_client.py +10 -3
  10. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/README.md +0 -0
  11. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/blank.html +0 -0
  12. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/content-script.js +0 -0
  13. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/manifest.json +0 -0
  14. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
  15. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
  16. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/wake-service-worker.html +0 -0
  17. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_extension/wake-service-worker.js +0 -0
  18. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_generated/__init__.py +0 -0
  19. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_sdk_identity.py +0 -0
  20. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/_validation.py +0 -0
  21. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/browser.py +0 -0
  22. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/browser_clipboard.py +0 -0
  23. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/browser_context.py +0 -0
  24. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/browserbase_services.py +0 -0
  25. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/browserbase_session.py +0 -0
  26. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/cdp_client.py +0 -0
  27. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/client_models.py +0 -0
  28. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/client_types.py +0 -0
  29. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/extension_assets.py +0 -0
  30. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/file_upload.py +0 -0
  31. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/locator.py +0 -0
  32. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/py.typed +0 -0
  33. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/response.py +0 -0
  34. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/stagehand.py +0 -0
  35. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/timeouts.py +0 -0
  36. {stagehand-4.2.0a0.dev1541 → stagehand-4.2.0a0.dev1544}/src/stagehand/webmcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stagehand
3
- Version: 4.2.0a0.dev1541
3
+ Version: 4.2.0a0.dev1544
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.2.0a0.dev1541"
3
+ version = "4.2.0a0.dev1544"
4
4
  description = "Browserbase's SDK for building browser agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -11,6 +11,7 @@ from ._generated.input_types import (
11
11
  ModelConfig,
12
12
  PageDragAndDropRoutePoint,
13
13
  PageEventName,
14
+ PagePDFMargin,
14
15
  PageScreenshotClip,
15
16
  RgbaColor,
16
17
  TelemetryConfig,
@@ -147,6 +148,7 @@ __all__ = [
147
148
  "ToolsRemovedListener",
148
149
  "WebMCPToolIdentity",
149
150
  "PageEventName",
151
+ "PagePDFMargin",
150
152
  "PageScreenshotClip",
151
153
  "ProtocolLocator",
152
154
  "RgbaColor",
@@ -7305,6 +7305,10 @@ var defaultOpaqueKeys = /* @__PURE__ */ new Set([
7305
7305
  "data",
7306
7306
  "userMetadata"
7307
7307
  ]);
7308
+ var apiAcronymWireKeys = {
7309
+ base_url: "base_URL",
7310
+ prefer_css_page_size: "prefer_CSS_page_size"
7311
+ };
7308
7312
  function wireSchema(schema, options = {}) {
7309
7313
  return preprocess((value) => isCaseable(value) ? camelcaseKeys(preserveApiAcronyms(value, options.opaqueKeys), {
7310
7314
  deep: true,
@@ -7347,7 +7351,7 @@ function preserveApiAcronyms(value, additionalOpaqueKeys = []) {
7347
7351
  function visit(entry) {
7348
7352
  if (Array.isArray(entry)) return entry.map(visit);
7349
7353
  if (!isRecord$1(entry)) return entry;
7350
- return Object.fromEntries(Object.entries(entry).map(([key, child]) => [key === "base_url" ? "base_URL" : key, opaqueKeys.has(key) ? child : visit(child)]));
7354
+ return Object.fromEntries(Object.entries(entry).map(([key, child]) => [Object.hasOwn(apiAcronymWireKeys, key) ? apiAcronymWireKeys[key] : key, opaqueKeys.has(key) ? child : visit(child)]));
7351
7355
  }
7352
7356
  return visit(value);
7353
7357
  }
@@ -7359,7 +7363,7 @@ function isRecord$1(value) {
7359
7363
  }
7360
7364
  var package_default$1 = {
7361
7365
  name: "@browserbasehq/stagehand-protocol",
7362
- version: "2.1.0-alpha-70f4e91f1983677c1e8ca9a86f0b738f828d059a",
7366
+ version: "2.1.0-alpha-5acf85448e65ece824e417d3de7eac884593f794",
7363
7367
  "private": true,
7364
7368
  type: "module",
7365
7369
  exports: {
@@ -8618,6 +8622,29 @@ var PageScreenshotOptionsSchema = strictObject({
8618
8622
  path: ["quality"]
8619
8623
  }).meta({ id: "PageScreenshotOptions" });
8620
8624
  var PageScreenshotParamsSchema = PageIdParamsSchema.extend({ options: PageScreenshotOptionsSchema.optional() }).meta({ id: "PageScreenshotParams" });
8625
+ var PagePDFMarginSchema = strictObject({
8626
+ top: number$1().nonnegative().optional(),
8627
+ bottom: number$1().nonnegative().optional(),
8628
+ left: number$1().nonnegative().optional(),
8629
+ right: number$1().nonnegative().optional()
8630
+ }).meta({ id: "PagePDFMargin" });
8631
+ var PagePDFOptionsSchema = strictObject({
8632
+ landscape: boolean().optional(),
8633
+ displayHeaderFooter: boolean().optional(),
8634
+ printBackground: boolean().optional(),
8635
+ scale: number$1().min(.1).max(2).optional(),
8636
+ width: number$1().positive().optional(),
8637
+ height: number$1().positive().optional(),
8638
+ margin: PagePDFMarginSchema.optional(),
8639
+ pageRanges: string().optional(),
8640
+ headerTemplate: string().optional(),
8641
+ footerTemplate: string().optional(),
8642
+ preferCSSPageSize: boolean().optional(),
8643
+ tagged: boolean().optional(),
8644
+ outline: boolean().optional(),
8645
+ timeout: number$1().nonnegative().max(2147473647).optional()
8646
+ }).meta({ id: "PagePDFOptions" });
8647
+ var PagePDFParamsSchema = PageIdParamsSchema.extend({ options: PagePDFOptionsSchema.optional() }).meta({ id: "PagePDFParams" });
8621
8648
  var PageSnapshotParamsSchema = PageIdParamsSchema.extend({ options: PageSnapshotOptionsSchema.optional() }).meta({ id: "PageSnapshotParams" });
8622
8649
  var PageSetViewportSizeParamsSchema = PageIdParamsSchema.extend({
8623
8650
  width: number$1().int().positive(),
@@ -8698,6 +8725,7 @@ var PageTitleResultSchema = string().meta({ id: "PageTitleResult" });
8698
8725
  var PageCloseResultSchema = strictObject({ closed: literal(true) }).meta({ id: "PageCloseResult" });
8699
8726
  var PageEvaluateResultSchema = strictObject({ value: json$1() }).meta({ id: "PageEvaluateResult" });
8700
8727
  var PageScreenshotResultSchema = strictObject({ data: base64().meta({ format: "byte" }) }).meta({ id: "PageScreenshotResult" });
8728
+ var PagePDFResultSchema = strictObject({ data: base64().meta({ format: "byte" }) }).meta({ id: "PagePDFResult" });
8701
8729
  var PageWaitForSelectorResultSchema = strictObject({ matched: boolean() }).meta({ id: "PageWaitForSelectorResult" });
8702
8730
  var LocatorClickResultSchema = strictObject({ clicked: literal(true) }).meta({ id: "LocatorClickResult" });
8703
8731
  var LocatorFillResultSchema = strictObject({ filled: literal(true) }).meta({ id: "LocatorFillResult" });
@@ -8977,6 +9005,11 @@ var StagehandMethods = {
8977
9005
  params: PageScreenshotParamsSchema,
8978
9006
  result: PageScreenshotResultSchema
8979
9007
  },
9008
+ pagePDF: {
9009
+ name: "page.pdf",
9010
+ params: PagePDFParamsSchema,
9011
+ result: PagePDFResultSchema
9012
+ },
8980
9013
  pageSnapshot: {
8981
9014
  name: "page.snapshot",
8982
9015
  params: PageSnapshotParamsSchema,
@@ -12323,24 +12356,20 @@ var Page$1 = class {
12323
12356
  }
12324
12357
  async screenshot(options) {
12325
12358
  const { path, mask, ...screenshotOptions } = options ?? {};
12326
- const bytes = decodeBase64((await this.rpcClient.send(StagehandMethods.pageScreenshot, {
12359
+ return await decodeCaptureResult((await this.rpcClient.send(StagehandMethods.pageScreenshot, {
12327
12360
  pageId: this.pageId,
12328
12361
  options: {
12329
12362
  ...screenshotOptions,
12330
12363
  ...mask ? { mask: mask.map((locator) => locator.descriptor) } : {}
12331
12364
  }
12332
- })).data, "page.screenshot");
12333
- if (path) {
12334
- const moduleName = "node:fs/promises";
12335
- const { writeFile } = await __vitePreload(() => import(
12336
- /* @vite-ignore */
12337
- moduleName
12338
- ), []).catch(() => {
12339
- throw new TypeError("page.screenshot(): path is only supported in Node.js; omit path to receive screenshot bytes");
12340
- });
12341
- await writeFile(path, bytes);
12342
- }
12343
- return bytes;
12365
+ })).data, "screenshot", path);
12366
+ }
12367
+ async pdf(options) {
12368
+ const { path, ...pdfOptions } = options ?? {};
12369
+ return await decodeCaptureResult((await this.rpcClient.send(StagehandMethods.pagePDF, {
12370
+ pageId: this.pageId,
12371
+ options: pdfOptions
12372
+ })).data, "pdf", path);
12344
12373
  }
12345
12374
  async snapshot(options) {
12346
12375
  return await this.rpcClient.send(StagehandMethods.pageSnapshot, {
@@ -12371,6 +12400,20 @@ var Page$1 = class {
12371
12400
  });
12372
12401
  }
12373
12402
  };
12403
+ async function decodeCaptureResult(data, method, path) {
12404
+ const bytes = decodeBase64(data, `page.${method}`);
12405
+ if (path !== void 0) {
12406
+ const moduleName = "node:fs/promises";
12407
+ const { writeFile } = await __vitePreload(() => import(
12408
+ /* @vite-ignore */
12409
+ moduleName
12410
+ ), []).catch(() => {
12411
+ throw new TypeError(`page.${method}(): path is only supported in Node.js; omit path to receive ${method === "pdf" ? "PDF" : "screenshot"} bytes`);
12412
+ });
12413
+ await writeFile(path, bytes);
12414
+ }
12415
+ return bytes;
12416
+ }
12374
12417
  function reportPageEventListenerError(error) {
12375
12418
  process.emitWarning(error instanceof Error ? error.message : String(error), { code: "STAGEHAND_PAGE_EVENT_LISTENER_ERROR" });
12376
12419
  }
@@ -12931,6 +12974,10 @@ function createPageController(runtime) {
12931
12974
  logger.debug("page.screenshot", {});
12932
12975
  return runtime.pageScreenshot(params);
12933
12976
  }
12977
+ async function pdf(params, { logger }) {
12978
+ logger.debug("page.pdf", {});
12979
+ return runtime.pagePDF(params);
12980
+ }
12934
12981
  async function snapshot(params, { logger }) {
12935
12982
  logger.debug("page.snapshot", {});
12936
12983
  return runtime.pageSnapshot(params);
@@ -12990,6 +13037,7 @@ function createPageController(runtime) {
12990
13037
  waitForTimeout,
12991
13038
  waitForSelector,
12992
13039
  screenshot,
13040
+ pdf,
12993
13041
  snapshot,
12994
13042
  webMCPTools,
12995
13043
  webMCPInvokeTool,
@@ -16290,7 +16338,7 @@ function resolveMaskRect(maskToken) {
16290
16338
  //#region understudy/screenshotUtils.ts
16291
16339
  var screenshotQueues = /* @__PURE__ */ new WeakMap();
16292
16340
  /** Serialize page mutations, or the browser-wide activation/capture critical section. */
16293
- async function withScreenshotLock(owner, capture, timeout) {
16341
+ async function withScreenshotLock(owner, capture, timeout, operation = "screenshot") {
16294
16342
  const queue = screenshotQueues.get(owner) ?? {
16295
16343
  tail: Promise.resolve(),
16296
16344
  blocked: new AbortController()
@@ -16299,8 +16347,9 @@ async function withScreenshotLock(owner, capture, timeout) {
16299
16347
  const controller = new AbortController();
16300
16348
  let started = false;
16301
16349
  const timer = typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0 ? setTimeout(() => {
16302
- controller.abort(new TimeoutError("screenshot", timeout));
16303
- if (started) queue.blocked.abort(/* @__PURE__ */ new Error("screenshot: a previous timed-out capture is still recovering"));
16350
+ const error = new TimeoutError(operation, timeout);
16351
+ controller.abort(error);
16352
+ if (started) queue.blocked.abort(new Error(`A previous capture is still recovering: ${error.message}`, { cause: error }));
16304
16353
  }, Math.min(timeout, 2147483647)) : void 0;
16305
16354
  const pending = queue.tail.then(() => {
16306
16355
  queue.blocked.signal.throwIfAborted();
@@ -51568,6 +51617,7 @@ var RPCRouter = class {
51568
51617
  case "page.wait_for_timeout": return this.pageController.waitForTimeout(parseParams(StagehandMethods.pageWaitForTimeout, request.params), context);
51569
51618
  case "page.wait_for_selector": return this.pageController.waitForSelector(parseParams(StagehandMethods.pageWaitForSelector, request.params), context);
51570
51619
  case "page.screenshot": return this.pageController.screenshot(parseParams(StagehandMethods.pageScreenshot, request.params), context);
51620
+ case "page.pdf": return this.pageController.pdf(parseParams(StagehandMethods.pagePDF, request.params), context);
51571
51621
  case "page.snapshot": return this.pageController.snapshot(parseParams(StagehandMethods.pageSnapshot, request.params), context);
51572
51622
  case "page.webmcp_tools": return this.pageController.webMCPTools(parseParams(StagehandMethods.pageWebMCPTools, request.params), context);
51573
51623
  case "page.webmcp_invoke_tool": return this.pageController.webMCPInvokeTool(parseParams(StagehandMethods.pageWebMCPInvokeTool, request.params), context);
@@ -53961,7 +54011,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
53961
54011
  };
53962
54012
  var package_default = {
53963
54013
  name: "@browserbasehq/stagehand-extension",
53964
- version: "1.1.0-alpha-70f4e91f1983677c1e8ca9a86f0b738f828d059a",
54014
+ version: "1.1.0-alpha-5acf85448e65ece824e417d3de7eac884593f794",
53965
54015
  "private": true,
53966
54016
  type: "module",
53967
54017
  scripts: {
@@ -56387,6 +56437,25 @@ var Page = class Page {
56387
56437
  };
56388
56438
  return await withScreenshotLock(this, exec, opts.timeout);
56389
56439
  }
56440
+ /** Keep the PDF base64-encoded for transport; SDKs decode it to bytes. */
56441
+ async pdf(options) {
56442
+ const { timeout = 3e4, width, height, margin, tagged = false, outline = false, ...printOptions } = options ?? {};
56443
+ return await withScreenshotLock(this, async () => {
56444
+ const { data } = await this.mainSession.send("Page.printToPDF", {
56445
+ ...printOptions,
56446
+ paperWidth: width,
56447
+ paperHeight: height,
56448
+ marginTop: margin?.top ?? 0,
56449
+ marginBottom: margin?.bottom ?? 0,
56450
+ marginLeft: margin?.left ?? 0,
56451
+ marginRight: margin?.right ?? 0,
56452
+ generateTaggedPDF: tagged,
56453
+ generateDocumentOutline: outline,
56454
+ transferMode: "ReturnAsBase64"
56455
+ });
56456
+ return { data };
56457
+ }, timeout, "pdf");
56458
+ }
56390
56459
  /**
56391
56460
  * specifies additional HTTP headers to be included in every request sent by
56392
56461
  * the root CDP session of the page, and all of its child CDP sessions.
@@ -57659,6 +57728,9 @@ var StagehandRuntime = class {
57659
57728
  }
57660
57729
  return { data: bytesToBase64(await page.screenshot(options)) };
57661
57730
  }
57731
+ async pagePDF(params) {
57732
+ return await this.resolvePage(params.pageId).pdf(params.options);
57733
+ }
57662
57734
  async pageSnapshot(params) {
57663
57735
  return await this.resolvePage(params.pageId).snapshot(params.options);
57664
57736
  }
@@ -781,6 +781,39 @@ class PageOffParams(TypedDict):
781
781
  subscription_id: str
782
782
 
783
783
 
784
+ class PagePDFMargin(TypedDict):
785
+ top: NotRequired[float]
786
+ bottom: NotRequired[float]
787
+ left: NotRequired[float]
788
+ right: NotRequired[float]
789
+
790
+
791
+ class PagePDFOptions(TypedDict):
792
+ landscape: NotRequired[bool]
793
+ display_header_footer: NotRequired[bool]
794
+ print_background: NotRequired[bool]
795
+ scale: NotRequired[float]
796
+ width: NotRequired[float]
797
+ height: NotRequired[float]
798
+ margin: NotRequired[PagePDFMargin]
799
+ page_ranges: NotRequired[str]
800
+ header_template: NotRequired[str]
801
+ footer_template: NotRequired[str]
802
+ prefer_css_page_size: NotRequired[bool]
803
+ tagged: NotRequired[bool]
804
+ outline: NotRequired[bool]
805
+ timeout: NotRequired[float]
806
+
807
+
808
+ class PagePDFParams(TypedDict):
809
+ page_id: str
810
+ options: NotRequired[PagePDFOptions]
811
+
812
+
813
+ class PagePDFResult(TypedDict):
814
+ data: str
815
+
816
+
784
817
  class PageRef(TypedDict):
785
818
  page_id: str
786
819
  url: NotRequired[str]
@@ -1677,6 +1677,61 @@ class PageOnParams(WireModel):
1677
1677
  event: PageSubscriptionEventName
1678
1678
 
1679
1679
 
1680
+ class PagePDFMargin(WireModel):
1681
+ model_config = ConfigDict(
1682
+ extra="forbid",
1683
+ validate_by_name=True,
1684
+ )
1685
+ top: Annotated[Optional[StrictFloat], Field(ge=0.0)] = None
1686
+ bottom: Annotated[Optional[StrictFloat], Field(ge=0.0)] = None
1687
+ left: Annotated[Optional[StrictFloat], Field(ge=0.0)] = None
1688
+ right: Annotated[Optional[StrictFloat], Field(ge=0.0)] = None
1689
+
1690
+
1691
+ class PagePDFOptions(WireModel):
1692
+ model_config = ConfigDict(
1693
+ extra="forbid",
1694
+ validate_by_name=True,
1695
+ )
1696
+ landscape: Optional[StrictBool] = None
1697
+ display_header_footer: Optional[StrictBool] = None
1698
+ print_background: Optional[StrictBool] = None
1699
+ scale: Annotated[Optional[StrictFloat], Field(ge=0.1, le=2.0)] = None
1700
+ width: Annotated[Optional[StrictFloat], Field(gt=0.0)] = None
1701
+ height: Annotated[Optional[StrictFloat], Field(gt=0.0)] = None
1702
+ margin: Optional[PagePDFMargin] = None
1703
+ page_ranges: Optional[StrictStr] = None
1704
+ header_template: Optional[StrictStr] = None
1705
+ footer_template: Optional[StrictStr] = None
1706
+ prefer_css_page_size: Optional[StrictBool] = None
1707
+ tagged: Optional[StrictBool] = None
1708
+ outline: Optional[StrictBool] = None
1709
+ timeout: Annotated[Optional[StrictFloat], Field(ge=0.0, le=2147473647.0)] = None
1710
+
1711
+
1712
+ class PagePDFParams(WireModel):
1713
+ model_config = ConfigDict(
1714
+ extra="forbid",
1715
+ validate_by_name=True,
1716
+ )
1717
+ page_id: StrictStr
1718
+ options: Optional[PagePDFOptions] = None
1719
+
1720
+
1721
+ class PagePDFResult(WireModel):
1722
+ model_config = ConfigDict(
1723
+ extra="forbid",
1724
+ validate_by_name=True,
1725
+ )
1726
+ data: Annotated[
1727
+ StrictStr,
1728
+ Field(
1729
+ json_schema_extra={"contentEncoding": "base64"},
1730
+ pattern="^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$",
1731
+ ),
1732
+ ]
1733
+
1734
+
1680
1735
  class PageRef(WireModel):
1681
1736
  model_config = ConfigDict(
1682
1737
  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.1.0-alpha-70f4e91f1983677c1e8ca9a86f0b738f828d059a'
4
+ STAGEHAND_PROTOCOL_VERSION = '2.1.0-alpha-5acf85448e65ece824e417d3de7eac884593f794'
@@ -14,6 +14,7 @@ from pydantic import JsonValue, TypeAdapter
14
14
  from ._generated.input_types import (
15
15
  PageDragAndDropRoutePoint,
16
16
  PageEventName,
17
+ PagePDFMargin,
17
18
  PageScreenshotClip,
18
19
  PageSubscriptionEventName,
19
20
  )
@@ -44,6 +45,9 @@ from ._generated.models import (
44
45
  PageNavigationResult,
45
46
  PageOffParams,
46
47
  PageOnParams,
48
+ PagePDFOptions,
49
+ PagePDFParams,
50
+ PagePDFResult,
47
51
  PageRef,
48
52
  PageReloadOptions,
49
53
  PageReloadParams,
@@ -613,6 +617,54 @@ class Page:
613
617
  Path(path).write_bytes(data)
614
618
  return data
615
619
 
620
+ async def pdf(
621
+ self,
622
+ *,
623
+ landscape: bool | None = None,
624
+ display_header_footer: bool | None = None,
625
+ print_background: bool | None = None,
626
+ scale: float | None = None,
627
+ width: float | None = None,
628
+ height: float | None = None,
629
+ margin: PagePDFMargin | None = None,
630
+ page_ranges: str | None = None,
631
+ header_template: str | None = None,
632
+ footer_template: str | None = None,
633
+ prefer_css_page_size: bool | None = None,
634
+ tagged: bool | None = None,
635
+ outline: bool | None = None,
636
+ timeout: float | None = None,
637
+ path: str | Path | None = None,
638
+ ) -> bytes:
639
+ params = PagePDFParams(page_id=self.page_id)
640
+ options = PagePDFOptions.model_validate({
641
+ name: value
642
+ for name, value in (
643
+ ("landscape", landscape),
644
+ ("display_header_footer", display_header_footer),
645
+ ("print_background", print_background),
646
+ ("scale", scale),
647
+ ("width", width),
648
+ ("height", height),
649
+ ("margin", margin),
650
+ ("page_ranges", page_ranges),
651
+ ("header_template", header_template),
652
+ ("footer_template", footer_template),
653
+ ("prefer_css_page_size", prefer_css_page_size),
654
+ ("tagged", tagged),
655
+ ("outline", outline),
656
+ ("timeout", timeout),
657
+ )
658
+ if value is not None
659
+ })
660
+ if options.model_fields_set:
661
+ params.options = options
662
+ result = await self._rpc_client.send("page.pdf", params, PagePDFResult)
663
+ data = base64.b64decode(result.data, validate=True)
664
+ if path is not None:
665
+ Path(path).write_bytes(data)
666
+ return data
667
+
616
668
  async def snapshot(self, *, include_iframes: bool | None = None) -> SnapshotResult:
617
669
  params = PageSnapshotParams(page_id=self.page_id)
618
670
  if include_iframes is not None:
@@ -29,6 +29,7 @@ _DEFAULT_OPERATION_TIMEOUT_MS = {
29
29
  "page.go_forward": 15_000,
30
30
  "page.wait_for_load_state": 15_000,
31
31
  "page.wait_for_selector": 30_000,
32
+ "page.pdf": 30_000,
32
33
  "page.webmcp_tools": 1_000,
33
34
  }
34
35
  _UNBOUNDED_BY_DEFAULT_METHODS = {
@@ -207,7 +208,8 @@ class RPCClient:
207
208
  tracestate=trace_context.get("tracestate"),
208
209
  )
209
210
 
210
- response_timeout = asyncio.timeout(_rpc_response_timeout_seconds(method, parsed_params))
211
+ response_timeout_seconds = _rpc_response_timeout_seconds(method, parsed_params)
212
+ response_timeout = asyncio.timeout(response_timeout_seconds)
211
213
  try:
212
214
  try:
213
215
  async with response_timeout:
@@ -219,8 +221,10 @@ class RPCClient:
219
221
  )
220
222
  return await response
221
223
  except TimeoutError as error:
222
- if response_timeout.expired():
223
- raise TimeoutError(f"RPC response timed out: {method}") from error
224
+ if response_timeout.expired() and response_timeout_seconds is not None:
225
+ raise TimeoutError(
226
+ f"RPC response timed out after {response_timeout_seconds:g}s: {method}"
227
+ ) from error
224
228
  raise
225
229
  finally:
226
230
  self._pending.pop(request_id, None)
@@ -543,6 +547,7 @@ def _rpc_response_timeout_seconds(method: str, params: BaseModel) -> float | Non
543
547
  "page.go_back",
544
548
  "page.go_forward",
545
549
  "page.screenshot",
550
+ "page.pdf",
546
551
  "page.wait_for_selector",
547
552
  "page.webmcp_tools",
548
553
  "page.webmcp_invocation_result",
@@ -554,6 +559,8 @@ def _rpc_response_timeout_seconds(method: str, params: BaseModel) -> float | Non
554
559
  operation_timeout_ms = _numeric_property(params, "ms")
555
560
 
556
561
  if operation_timeout_ms is not None:
562
+ if method == "page.pdf" and operation_timeout_ms == 0:
563
+ return None
557
564
  return (_RPC_RESPONSE_GRACE_MS + max(0, operation_timeout_ms)) / 1_000
558
565
 
559
566
  if (default_timeout_ms := _DEFAULT_OPERATION_TIMEOUT_MS.get(method)) is not None: