Skip to content

Replace the Python Stagehand lifecycle with Stagehand.create - #2545

Open
miguelg719 wants to merge 8 commits into
feat/stagehand-py-browser-factoriesfrom
feat/stagehand-py-create-lifecycle
Open

Replace the Python Stagehand lifecycle with Stagehand.create#2545
miguelg719 wants to merge 8 commits into
feat/stagehand-py-browser-factoriesfrom
feat/stagehand-py-create-lifecycle

Conversation

@miguelg719

@miguelg719 miguelg719 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

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.

  • makes Stagehand() unconstructible directly (module-private token; TypeError points at Stagehand.create)
  • removes init(), async-with, the flattened browser constructor kwargs, the BrowserSource union models, and browser_source.py
  • exports local_browser, browserbase, and StagehandBrowser from the package root
  • stagehand.browser returns the exact handle passed to create; a failed create releases the claim so the same handle can be retried
  • stagehand.close() never touches the browser or CDP socket — browser/session lifetime is exclusively browser.close()
  • worker init keeps the exact StagehandInitParams wire shape; handle metadata overrides caller api_key, and local handles omit browser entirely
  • migrates all 7 examples, README, and the CI wheel-smoke script to launch → create → stagehand.close()browser.close() (browser close in the outermost finally)
  • updates the Python ast-grep example-parity patterns to the Stagehand.create shape

Reviewer focus

  1. stagehand.close() stops the runtime; browser.close() owns browser cleanup — under no configuration does Stagehand close the transport or process.
  2. Browser acquisition options stay client-side and never cross the RPC boundary.
  3. One-shot asyncio semantics: memoized close(), claim release under failed create, concurrent-close safety.

Stack

Verification

  • full Python package gates green: generate.py --check, ruff format --check, ruff check, ty check, pytest (rewritten lifecycle suite incl. wire-shape, claim-retry, and concurrent-close tests)
  • repo pnpm run test:unit green (ast-grep example-parity + sdk-parity against the migrated sources)
  • changeset check passed; wheel smoke exercised by CI python-wheel-smoke

Summary by cubic

Make await Stagehand.create(browser=...) the only way to start the Python client. Adds local_browser and browserbase factories, moves browser lifetime to the browser handle, and makes close/cancel safe on Python 3.12’s eager task factory.

  • Refactors

    • Stagehand() is no longer constructible; use Stagehand.create.
    • Removed init(), async-with usage, BrowserSource models, and browser_source.py; ResolvedBrowserSource now lives in browser.py with dead fields removed.
    • Exported local_browser, browserbase, and StagehandBrowser from stagehand.
    • stagehand.browser returns the exact handle passed to create; failed or canceled create releases the claim and detaches the RPC client with close_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.
    • Removed connect_rpc_client; updated README, examples, tests, smoke script, and ast-grep rules (Python now requires Stagehand.create).
  • Migration

    • Launch a browser: browser = await local_browser.launch(...) or browser = await browserbase.launch(api_key=...).
    • Create the client: stagehand = await Stagehand.create(browser=browser, ...).
    • Close in order: await stagehand.close() then await browser.close() (put browser close in the outermost finally).
    • Remove any async with Stagehand(...) and calls to init().

Written for commit ce6f946. Summary will update on new commits.

Review in cubic

…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.
@changeset-bot

changeset-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ce6f946

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@miguelg719
miguelg719 marked this pull request as ready for review August 1, 2026 04:12

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 22 files

Confidence score: 2/5

  • In packages/sdk-python/src/stagehand/stagehand.py, the browserbase.launch()/browserbase.connect() path raises NotImplementedError before Stagehand.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 to browserbase.launch(api_key=...) crashes because BrowserbaseBrowser is 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
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/sdk-python/src/stagehand/stagehand.py
Comment thread packages/sdk-python/examples/caching.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant