stagehand 4.1.0a0.dev1497__tar.gz → 4.1.0a0.dev1499__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.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/PKG-INFO +1 -1
  2. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/pyproject.toml +1 -1
  3. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/__init__.py +2 -0
  4. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/service-worker.js +2 -2
  5. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_generated/protocol_version.py +1 -1
  6. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/cdp_client.py +131 -24
  7. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/README.md +0 -0
  8. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/blank.html +0 -0
  9. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/content-script.js +0 -0
  10. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/manifest.json +0 -0
  11. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
  12. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
  13. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/wake-service-worker.html +0 -0
  14. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_extension/wake-service-worker.js +0 -0
  15. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_generated/__init__.py +0 -0
  16. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_generated/input_types.py +0 -0
  17. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_generated/models.py +0 -0
  18. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_sdk_identity.py +0 -0
  19. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/_validation.py +0 -0
  20. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/browser.py +0 -0
  21. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/browser_clipboard.py +0 -0
  22. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/browser_context.py +0 -0
  23. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/browserbase_services.py +0 -0
  24. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/browserbase_session.py +0 -0
  25. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/client_models.py +0 -0
  26. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/client_types.py +0 -0
  27. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/extension_assets.py +0 -0
  28. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/file_upload.py +0 -0
  29. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/locator.py +0 -0
  30. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/page.py +0 -0
  31. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/py.typed +0 -0
  32. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/response.py +0 -0
  33. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/rpc_client.py +0 -0
  34. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/stagehand.py +0 -0
  35. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/timeouts.py +0 -0
  36. {stagehand-4.1.0a0.dev1497 → stagehand-4.1.0a0.dev1499}/src/stagehand/webmcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stagehand
3
- Version: 4.1.0a0.dev1497
3
+ Version: 4.1.0a0.dev1499
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.1.0a0.dev1497"
3
+ version = "4.1.0a0.dev1499"
4
4
  description = "Browserbase's SDK for building browser agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -58,6 +58,7 @@ from ._generated.models import (
58
58
  from .browser import StagehandBrowser, browserbase, local_browser
59
59
  from .browser_clipboard import BrowserClipboard
60
60
  from .browser_context import BrowserContext
61
+ from .cdp_client import StagehandRuntimeIncompatibleError
61
62
  from .client_models import (
62
63
  BrowserbaseFetchResult,
63
64
  BrowserbaseSearchResult,
@@ -147,6 +148,7 @@ __all__ = [
147
148
  "StagehandClientLoggingConfig",
148
149
  "StagehandMetrics",
149
150
  "StagehandResultMetadata",
151
+ "StagehandRuntimeIncompatibleError",
150
152
  "State",
151
153
  "TelemetryConfig",
152
154
  "Variables",
@@ -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-5beaca74cd75dbe7ad09cfb0a2b287cad03ed54e",
7362
+ version: "2.0.0-alpha-67a46682bf06f5607180470007a65e5811c09bcf",
7363
7363
  "private": true,
7364
7364
  type: "module",
7365
7365
  exports: {
@@ -53493,7 +53493,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
53493
53493
  };
53494
53494
  var package_default = {
53495
53495
  name: "@browserbasehq/stagehand-extension",
53496
- version: "1.0.2-alpha-5beaca74cd75dbe7ad09cfb0a2b287cad03ed54e",
53496
+ version: "1.0.2-alpha-67a46682bf06f5607180470007a65e5811c09bcf",
53497
53497
  "private": true,
53498
53498
  type: "module",
53499
53499
  scripts: {
@@ -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-5beaca74cd75dbe7ad09cfb0a2b287cad03ed54e'
4
+ STAGEHAND_PROTOCOL_VERSION = '2.0.0-alpha-67a46682bf06f5607180470007a65e5811c09bcf'
@@ -7,7 +7,7 @@ import time
7
7
  from collections.abc import Mapping
8
8
  from contextlib import suppress
9
9
  from dataclasses import dataclass
10
- from typing import Any, Protocol, cast
10
+ from typing import Any, Literal, Protocol, cast
11
11
  from urllib.request import urlopen
12
12
 
13
13
  from stagehand._generated.protocol_version import (
@@ -59,42 +59,136 @@ def _callback_source_from_message(message: dict[str, object]) -> str | None:
59
59
  return source
60
60
 
61
61
 
62
- def _negotiate_runtime(marker: object) -> tuple[bool, str]:
63
- """Return (compatible, detail). Never raises: a malformed marker is just incompatible."""
62
+ RUNTIME_INCOMPATIBLE_REMEDIATION = (
63
+ "Upgrade the Stagehand SDK and the Stagehand extension together so their protocol majors "
64
+ "match, or start the session with the extension bundled in this SDK."
65
+ )
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class _RuntimeNegotiation:
70
+ """Outcome of reading the runtime marker the extension publishes.
71
+
72
+ ``kind`` is three-way on purpose: ``"unknown"`` means the marker is absent or not yet
73
+ readable and the caller should keep polling, while ``"incompatible"`` means the marker
74
+ parsed but this client can never talk to that runtime, so polling is pointless.
75
+ """
76
+
77
+ kind: Literal["compatible", "incompatible", "unknown"]
78
+ detail: str
79
+ reason: str | None = None
80
+ protocol_version: str | None = None
81
+ server_name: str | None = None
82
+ server_version: str | None = None
83
+
84
+ @property
85
+ def compatible(self) -> bool:
86
+ return self.kind == "compatible"
87
+
88
+
89
+ class StagehandRuntimeIncompatibleError(RuntimeError):
90
+ """The connected Stagehand extension speaks a protocol this SDK cannot use.
91
+
92
+ Raised on the first readiness poll that observes the incompatible marker; initialization
93
+ does not wait for the timeout because the extension will not change its protocol version
94
+ while the session is open.
95
+ """
96
+
97
+ def __init__(self, negotiation: _RuntimeNegotiation) -> None:
98
+ self.reason = negotiation.reason or "protocol-incompatible"
99
+ self.client_protocol_version = STAGEHAND_PROTOCOL_VERSION
100
+ self.reported_protocol_version = negotiation.protocol_version
101
+ self.server_name = negotiation.server_name
102
+ self.server_version = negotiation.server_version
103
+ self.remediation = RUNTIME_INCOMPATIBLE_REMEDIATION
104
+ super().__init__(
105
+ f"Incompatible Stagehand runtime: {negotiation.detail}; "
106
+ f"client protocol {self.client_protocol_version}, "
107
+ f"reported protocol {self.reported_protocol_version}, "
108
+ f"server {self.server_name}/{self.server_version}. "
109
+ f"{self.remediation}"
110
+ )
111
+
112
+
113
+ def _negotiate_runtime(marker: object) -> _RuntimeNegotiation:
114
+ """Classify the runtime marker. Never raises: hostile input is just ``unknown``."""
64
115
  if not isinstance(marker, Mapping):
65
- return False, "no Stagehand runtime marker"
116
+ return _RuntimeNegotiation("unknown", "no Stagehand runtime marker")
66
117
 
67
118
  server_info = marker.get("serverInfo")
68
- name = server_info.get("name") if isinstance(server_info, Mapping) else None
69
- if name != _RUNTIME_NAME:
70
- return False, f"serverInfo.name={name!r}"
119
+ if not isinstance(server_info, Mapping):
120
+ return _RuntimeNegotiation("unknown", f"serverInfo={server_info!r}")
121
+ name = server_info.get("name")
122
+ version = server_info.get("version")
123
+ # Mirrors the protocol's ImplementationInfoSchema: both fields are non-empty strings.
124
+ # A marker missing either is malformed, not a foreign runtime, so keep polling.
125
+ if not isinstance(name, str) or not name or not isinstance(version, str) or not version:
126
+ return _RuntimeNegotiation(
127
+ "unknown", f"serverInfo.name={name!r} serverInfo.version={version!r}"
128
+ )
71
129
 
72
130
  protocol_version = marker.get("protocolVersion")
73
- if not isinstance(protocol_version, str):
74
- return False, f"protocolVersion={protocol_version!r}"
75
- compatibility = _protocol_compatibility(STAGEHAND_PROTOCOL_VERSION, protocol_version)
76
- if compatibility is not None:
77
- return False, compatibility
131
+ if not isinstance(protocol_version, str) or not protocol_version:
132
+ return _RuntimeNegotiation("unknown", f"protocolVersion={protocol_version!r}")
78
133
 
79
- return True, f"protocolVersion={protocol_version}"
134
+ if name != _RUNTIME_NAME:
135
+ return _RuntimeNegotiation(
136
+ "incompatible",
137
+ f'Runtime name mismatch: expected "{_RUNTIME_NAME}", server reported "{name}"',
138
+ reason="runtime-name-mismatch",
139
+ protocol_version=protocol_version,
140
+ server_name=name,
141
+ server_version=version,
142
+ )
143
+
144
+ incompatibility = _protocol_compatibility(STAGEHAND_PROTOCOL_VERSION, protocol_version)
145
+ if incompatibility is not None:
146
+ reason, detail = incompatibility
147
+ return _RuntimeNegotiation(
148
+ "incompatible",
149
+ detail,
150
+ reason=reason,
151
+ protocol_version=protocol_version,
152
+ server_name=name,
153
+ server_version=version,
154
+ )
155
+
156
+ return _RuntimeNegotiation(
157
+ "compatible",
158
+ f"protocolVersion={protocol_version}",
159
+ protocol_version=protocol_version,
160
+ server_name=name,
161
+ server_version=version,
162
+ )
80
163
 
81
164
 
82
- def _protocol_compatibility(client_version: str, server_version: str) -> str | None:
165
+ def _protocol_compatibility(client_version: str, server_version: str) -> tuple[str, str] | None:
166
+ """Return ``None`` when compatible, otherwise ``(reason, detail)``."""
83
167
  client = _PROTOCOL_SEMVER_PATTERN.fullmatch(client_version)
84
168
  server = _PROTOCOL_SEMVER_PATTERN.fullmatch(server_version)
85
169
  if client is None or server is None:
86
- return f"invalid protocol version: client={client_version!r} server={server_version!r}"
170
+ return (
171
+ "protocol-invalid-version",
172
+ f"Invalid protocol version: client {client_version}, server {server_version}",
173
+ )
87
174
  if client.group(4) is not None or server.group(4) is not None:
88
175
  if client_version != server_version:
89
176
  return (
90
- "protocol prereleases must match exactly: "
91
- f"client={client_version} server={server_version}"
177
+ "protocol-prerelease-mismatch",
178
+ "Protocol prereleases must match exactly: "
179
+ f"client {client_version}, server {server_version}",
92
180
  )
93
181
  return None
94
182
  if client.group(1) != server.group(1):
95
- return f"protocol major mismatch: client={client_version} server={server_version}"
183
+ return (
184
+ "protocol-major-mismatch",
185
+ f"Protocol major mismatch: client {client_version}, server {server_version}",
186
+ )
96
187
  if int(server.group(2)) < int(client.group(2)):
97
- return f"server protocol {server_version} is older than client requirement {client_version}"
188
+ return (
189
+ "protocol-server-too-old",
190
+ f"Server protocol {server_version} is older than client requirement {client_version}",
191
+ )
98
192
  return None
99
193
 
100
194
 
@@ -474,7 +568,17 @@ class CDPClient:
474
568
  "Target.closeTarget", {"targetId": activation_target_id}
475
569
  )
476
570
 
477
- async def _wait_for_runtime_receiver(self, session_id: str) -> None:
571
+ async def _wait_for_runtime_receiver(
572
+ self,
573
+ session_id: str,
574
+ *,
575
+ allow_fallback_install: bool = False,
576
+ ) -> None:
577
+ """Poll until the extension runtime is ready.
578
+
579
+ ``allow_fallback_install`` is reserved for flows that can replace an incompatible
580
+ preloaded extension; by default an incompatible marker fails on the first poll.
581
+ """
478
582
  while True:
479
583
  try:
480
584
  evaluated = await self.send_command(
@@ -485,17 +589,20 @@ class CDPClient:
485
589
  },
486
590
  session_id=session_id,
487
591
  )
592
+ except Exception:
593
+ evaluated = None
594
+ if evaluated is not None:
488
595
  exception = evaluated.get("exceptionDetails")
489
596
  if not isinstance(exception, Mapping):
490
597
  result = evaluated.get("result")
491
598
  value = result.get("value") if isinstance(result, Mapping) else None
492
599
  if isinstance(value, Mapping):
493
600
  has_receiver = value.get("hasReceiver") is True
494
- compatible, _ = _negotiate_runtime(value.get("marker"))
495
- if compatible and has_receiver:
601
+ negotiation = _negotiate_runtime(value.get("marker"))
602
+ if negotiation.kind == "incompatible" and not allow_fallback_install:
603
+ raise StagehandRuntimeIncompatibleError(negotiation)
604
+ if negotiation.compatible and has_receiver:
496
605
  return
497
- except Exception:
498
- pass
499
606
  await asyncio.sleep(0.1)
500
607
 
501
608
  def _schedule_best_effort_command(self, method: str, params: Mapping[str, object]) -> None: