Replace the Python Stagehand lifecycle with Stagehand.create - #2545
Merged
Conversation
…, and RPC transport detach
…review - Mark the handle closed synchronously when close() is requested so a pending close cannot race a claim (close() now memoizes the task on call and returns a shielded awaitable). - Run CDP-client and owned-source cleanup independently on connect failure, aggregating failures (BaseExceptionGroup specializes to ExceptionGroup for ordinary errors), and guarantee owned-source close in the handle close path via try/finally. - Catch BaseException in _connect_browser so cancellation during CDP connect/download configuration no longer leaks a launched Chrome. - Raise TypeError (matching the legacy constructor) for unpaired viewport_width/viewport_height. - Validate nested proxy/viewport values strictly so bool ints and bytes strings are rejected instead of coerced.
…Stagehand.create + browser factories
…ad source fields Review follow-ups on the create-lifecycle swap: - Stagehand.close() no longer memoizes via a recursive asyncio.create_task(self.close()) call, which infinitely recursed (RecursionError) under asyncio.eager_task_factory on Python 3.12+; the body now runs in a nested close_impl coroutine, mirroring the TS memoized-promise shape while keeping the stagehand.close RPC inside the public method for the ast-grep sdk-parity rule. - Remove the dead ResolvedBrowserSource fields (resident_browser_connection, cdp_headers, write-only connect_timeout_ms) left over from the browser_source.py deletion. - Restore cancellation coverage: cancelling Stagehand.create releases the claim, detaches the RPC client with close_transport=False, and leaves the browser open for retry; add a 3.12+-gated regression test running close() under the eager task factory.
|
miguelg719
marked this pull request as ready for review
August 1, 2026 04:12
Contributor
There was a problem hiding this comment.
2 issues found across 22 files
Confidence score: 2/5
- In
packages/sdk-python/src/stagehand/stagehand.py, thebrowserbase.launch()/browserbase.connect()path raisesNotImplementedErrorbeforeStagehand.create()runs, which blocks Browserbase users from the required handle-based migration and causes immediate runtime failure — implement the Browserbase factory methods (or gate this path) before release. - In
packages/sdk-python/examples/caching.py, the first call tobrowserbase.launch(api_key=...)crashes becauseBrowserbaseBrowseris still a stub, so the example is currently broken and can mislead adopters validating caching flows — update the example to a working path or finish the Browserbase session implementation first.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/sdk-python/src/stagehand/stagehand.py">
<violation number="1" location="packages/sdk-python/src/stagehand/stagehand.py:91">
P1: Browserbase users cannot migrate to this required handle-based path: both `browserbase.launch()` and `browserbase.connect()` always raise before `Stagehand.create()` can run. Implement the Browserbase factories before removing the prior Browserbase construction route, or retain a working migration path.</violation>
</file>
<file name="packages/sdk-python/examples/caching.py">
<violation number="1" location="packages/sdk-python/examples/caching.py:29">
P1: This example will crash on its first line. `browserbase.launch(api_key=...)` is a stub in `BrowserbaseBrowser` that unconditionally raises `NotImplementedError("Browserbase sessions are not implemented yet")`, so `await browserbase.launch(...)` in caching.py fails before any browser is created — no session, and the subsequent `Stagehand.create`, page navigation, and caching demo never run. Browserbase session acquisition is not yet implemented in this client: local handles work fine (`local_browser.launch`), but this example was migrated to a factory that doesn't exist yet. Consider keeping caching.py on the local path, or leave the example using the not-yet-available browserbase path out of the migrated set until Browserbase acquisition lands.</violation>
</file>
Architecture diagram
sequenceDiagram
participant User as User Code
participant BrowserF as Browser Factory
participant Stagehand as Stagehand
participant RPC as RPC Client
participant Worker as Stagehand Worker
participant CDP as CDP/Transport
Note over User,CDP: NEW: Exclusive Stagehand.create() lifecycle
User->>BrowserF: await local_browser.launch() or browserbase.launch()
BrowserF-->>User: StagehandBrowser handle
User->>Stagehand: await Stagehand.create(browser=browser, ...)
Stagehand->>Stagehand: Validate handle via _BROWSER_TOKEN
alt Invalid handle
Stagehand-->>User: TypeError
end
Stagehand->>Stagehand: Build StagehandClientCreateConfig
Stagehand->>BrowserF: _claim_browser(handle)
BrowserF-->>Stagehand: _ClaimedBrowser (cdp_client, worker_init_metadata)
Stagehand->>CDP: Create CDPClient from claimed transport
Stagehand->>RPC: Create RPCClient(cdp_client, timeout)
Stagehand->>RPC: Send stagehand.init
Note over RPC,Worker: NEW: Wire shape uses worker_init_metadata
RPC->>Worker: stagehand.init(protocol_version, browser_cdp_url, api_key, model, log_level, browser_metadata)
alt Worker metadata defined
Note over Worker: Worker api_key overrides caller api_key
Note over Worker: Local browser omits browser field entirely
end
Worker-->>RPC: StagehandInitResult
RPC-->>Stagehand: Initialized
alt Init fails or is cancelled
Stagehand->>BrowserF: _release_browser(handle)
Stagehand->>RPC: Close RPC (detaches, no transport close)
Stagehand-->>User: Propagates error
else Success
Stagehand-->>User: Stagehand instance
end
Note over User,CDP: Runtime operations (unchanged)
User->>Stagehand: stagehand.act/extract/observe()
Stagehand->>RPC: stagehand.act/etc.
RPC->>Worker: RPC method call
Worker-->>RPC: Result
RPC-->>Stagehand: Result
Stagehand-->>User: Response
Note over User,CDP: NEW: Clean shutdown (explicit ordering)
User->>Stagehand: await stagehand.close()
Stagehand->>Stagehand: Memoized close (one-shot)
Stagehand->>RPC: stagehand.close
RPC->>Worker: stagehand.close
Worker-->>RPC: StagehandCloseResult
Stagehand->>RPC: Close RPC client (no transport close)
RPC->>CDP: close(close_transport=False)
Stagehand-->>User: done
User->>BrowserF: await browser.close()
BrowserF->>CDP: close transport/process
CDP-->>BrowserF: done
BrowserF-->>User: done
Note over User,CDP: NEW: stagehand.close() never touches browser or CDP socket
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…e args are ignored
…d-py-create-lifecycle
…le' into feat/stagehand-py-create-lifecycle
…d-py-create-lifecycle
…d-py-create-lifecycle
…d-py-create-lifecycle
akeimach
approved these changes
Aug 3, 2026
…-browser-factories
…d-py-create-lifecycle # Conflicts: # packages/sdk-python/src/stagehand/client_models.py # packages/sdk-python/src/stagehand/stagehand.py # packages/sdk-python/tests/test_stagehand.py
…-browser-factories
…d-py-create-lifecycle
Base automatically changed from
feat/stagehand-py-browser-factories
to
v4-spike
August 3, 2026 19:36
miguelg719
added a commit
that referenced
this pull request
Aug 3, 2026
# why Ports the merged TypeScript browser-lifecycle stack (#2517–#2523) to the Python SDK. This bottom PR lands the entire new-lifecycle machinery as internal, unexported code so it can be reviewed in isolation while the legacy constructor/`init()` lifecycle stays fully green. # what changed - adds `stagehand/browser.py`: nominal `StagehandBrowser` handle (token-guarded construction, one-time Stagehand claim, idempotent memoized `close()`) - implements internal `local_browser.launch()` / `local_browser.connect()` factories that resolve only after the Stagehand extension service worker is ready - enforces launched-versus-connected ownership (`owns_source = launched and not keep_alive`) with `ExceptionGroup` cleanup semantics mirroring the TS `connectBrowser` - implements local downloads via root-session `Browser.setDownloadBehavior` (previously `NotImplementedError`) - reserves `browserbase.launch()` / `browserbase.connect()` API shape as `NotImplementedError` stubs — the Python SDK has never had Browserbase support; worker metadata is shaped `{api_key, browser: BrowserSessionMetadata}` so a future implementation needs no `Stagehand.create` changes - adds `RPCClient.close(close_transport=False)` so Stagehand can later detach from a browser-owned transport - slims `browser_source.py` to a legacy adapter re-binding the moved Chrome launcher; all existing monkeypatch seams keep working # intentionally not included - no public API change; `__init__.py` exports untouched - no changes to the `Stagehand` constructor, `init()`, or async-with - no changes under `_generated/` (already synced by the merged TS stack) # stack 1. **this PR — browser handle, factories, and transport detach (internal)** 2. #2545 — replace the constructor/`init()` lifecycle with `Stagehand.create` + browser factories 3. #2552 — implement Browserbase sessions # test plan - new `tests/test_browser.py` (374 lines): claim/release, ownership matrix, launch flags, downloads, connect extension routing - `RPCClient` `close_transport=False` coverage; existing legacy suites pass unchanged - full package gates green: `uv sync --locked`, `generate.py --check`, `ruff format --check`, `ruff check`, `ty check`, `pytest`
…-create-lifecycle # Conflicts: # packages/sdk-python/src/stagehand/browser.py # packages/sdk-python/src/stagehand/browser_source.py # packages/sdk-python/src/stagehand/client_models.py # packages/sdk-python/tests/test_rpc_client.py # rules/ast-grep/example-parity.test.ts # rules/ast-grep/sdk-parity.test.ts
miguelg719
added a commit
that referenced
this pull request
Aug 3, 2026
# why #2544 reserved `browserbase.launch` / `browserbase.connect` as `NotImplementedError` stubs. This PR makes them real, using the official `browserbase` PyPI SDK — the Python counterpart of the TS factories' `@browserbasehq/sdk` usage. # what changed - adds `browserbase>=1.15,<2` as a runtime dependency (`AsyncBrowserbase`), the SDK's first beyond pydantic + websockets - `browserbase.launch(api_key=..., ...)`: creates a session, provisioning the packaged Stagehand extension via an in-memory deterministic zip upload; caller-supplied extension IDs (top-level or `browser_settings`) suppress provisioning **and** cleanup - merges `user_metadata` with `{"stagehand": "true", "stagehand_sdk_language": "python"}` (Stagehand keys win) - ownership mirrors TS: `keep_alive ?? False` decides release-on-close; session release and owned-extension delete are idempotent per resource with retry-on-next-close; release errors take precedence - `browserbase.connect(api_key=..., session_id=...)`: `sessions.retrieve`-based, errors `Browserbase session is not available for connection` when no connect URL, never takes release ownership - adds a preloaded-extension discovery mode to `CDPClient.connect` (Browserbase sessions have the extension preinstalled; mirrors the TS/Go service-worker readiness polling) - worker init metadata carries `{api_key, browser: {session_id, region?}}`, overriding the caller's `api_key` per the TS spread order - sanitized `BrowserbaseSessionError` messages; official-SDK calls verified against the installed package (camelCase `fingerprint` wire mapping, `api_timeout` kwarg, `Content-Type` omitted on the body-less extension DELETE) - `examples/caching.py`'s existing `browserbase.launch` call now works as written — no example inventory changes (ast-grep parity) # stack 1. #2544 — browser handle, factories, and transport detach (internal) 2. #2545 — replace the constructor/`init()` lifecycle with `Stagehand.create` 3. **this PR — implement Browserbase sessions** # test plan - hand-rolled fake `_BrowserbaseAPI` seam tests: provisioning/suppression matrix, create-failure and cancellation cleanup, empty-id/URL handling, close idempotence + retry, connect paths, kwargs wire-shape tests (fingerprint camelCase, `api_timeout`), preloaded-extension CDP discovery tests - full package gates green: `uv lock --check`, `generate.py --check`, `ruff format --check`, `ruff check`, `ty check`, `pytest` (239 passed), sdist+wheel build, changeset + python-version sync checks, and every `just check`/`just test` recipe command run manually - live validation against the real Browserbase API (2026-08-02): extension upload -> session create -> preloaded-worker discovery -> `Stagehand.create` -> `stagehand.close` -> `browser.close` -> session `COMPLETED`, uploaded extension deleted (404 on retrieve). Caught and fixed one wire bug: `region: null` was rejected by the worker; the key is now omitted when unset (87369b4) <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds Browserbase session support to the Python browser factories via `browserbase.launch(...)` and `browserbase.connect(...)`. Improves preloaded extension discovery, deterministic extension packaging, and omits an unset Browserbase region from the `stagehand.init` payload. - **New Features** - `browserbase.launch(api_key, ...)` creates a session, uploads the Stagehand extension unless an `extension_id` is given, respects `keep_alive`, and sets worker metadata `{api_key, session_id, region}`. - `browserbase.connect(api_key, session_id, ...)` attaches to an existing session; uses a caller `extension_id` or the preloaded extension; never releases the session. - CDP discovers preloaded extensions with service worker readiness polling, detaches stale/incompatible or malformed workers, and exposes the discovered extension ID. - Adds `browserbase>=1.15,<2` as a runtime dependency. - **Bug Fixes** - Correct Browserbase wire mapping: camelCase fingerprint keys, `api_timeout`; reject empty `extension_id` (top-level or nested); omit `Content-Type` on extension delete; clearer errors. - Omit unset `region` from the `stagehand.init` payload (worker schema rejects `region: null`). - More robust cleanup and packaging: deterministic extension archives; idempotent release/delete with retry; delete owned extensions on create failure or cancellation. Docs clarify `keep_alive` behavior and extension retention. <sup>Written for commit 8234a67. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/browserbase/stagehand/pull/2552?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completes the Python port of the browser-lifecycle stack (#2517–#2523) by making
await Stagehand.create(browser=..., ...)the sole construction path, mirroring the TypeScript end state.Stagehand()unconstructible directly (module-private token;TypeErrorpoints atStagehand.create)init(), async-with, the flattened browser constructor kwargs, theBrowserSourceunion models, andbrowser_source.pylocal_browser,browserbase, andStagehandBrowserfrom the package rootstagehand.browserreturns the exact handle passed tocreate; a failedcreatereleases the claim so the same handle can be retriedstagehand.close()never touches the browser or CDP socket — browser/session lifetime is exclusivelybrowser.close()StagehandInitParamswire shape; handle metadata overrides callerapi_key, and local handles omitbrowserentirelystagehand.close()→browser.close()(browser close in the outermostfinally)Stagehand.createshapeReviewer focus
stagehand.close()stops the runtime;browser.close()owns browser cleanup — under no configuration does Stagehand close the transport or process.close(), claim release under failedcreate, concurrent-close safety.Stack
init()lifecycle withStagehand.createVerification
generate.py --check,ruff format --check,ruff check,ty check,pytest(rewritten lifecycle suite incl. wire-shape, claim-retry, and concurrent-close tests)pnpm run test:unitgreen (ast-grep example-parity + sdk-parity against the migrated sources)python-wheel-smokeSummary by cubic
Make
await Stagehand.create(browser=...)the only way to start the Python client. Addslocal_browserandbrowserbasefactories, moves browser lifetime to the browser handle, and hardens close/cancel and launch validation (incl. Python 3.12’s eager task factory).Refactors
Stagehand()is no longer constructible; useStagehand.create.init(), async-with usage, andBrowserSourcemodels; deletedbrowser_source.py.ResolvedBrowserSourcenow lives inbrowser.pywith dead fields removed.local_browser,browserbase, andStagehandBrowserfromstagehand.stagehand.browserreturns the exact handle passed tocreate. Failed or canceledcreatereleases the claim and detaches the RPC client withclose_transport=False.stagehand.close()stops the runtime only, is safe under Python 3.12’s eager task factory, and never closes the browser or CDP socket.connect_rpc_client; updated README, examples (incl.model_gateway.py), tests, smoke script, and ast-grep rules (Python now requiresStagehand.create).Migration
browser = await local_browser.launch(...)orbrowser = await browserbase.launch(api_key=...).stagehand = await Stagehand.create(browser=browser, ...).await stagehand.close()thenawait browser.close()(put browser close in the outermost finally).async with Stagehand(...)and calls toinit().Written for commit 9fdb16d. Summary will update on new commits.