Replace the Python Stagehand lifecycle with Stagehand.create - #2545
Open
miguelg719 wants to merge 8 commits into
Open
Replace the Python Stagehand lifecycle with Stagehand.create#2545miguelg719 wants to merge 8 commits into
miguelg719 wants to merge 8 commits into
Conversation
…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
…d-py-create-lifecycle
…le' into feat/stagehand-py-create-lifecycle
…d-py-create-lifecycle
…d-py-create-lifecycle
…d-py-create-lifecycle
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 makes close/cancel safe on Python 3.12’s eager task factory.Refactors
Stagehand()is no longer constructible; useStagehand.create.init(), async-with usage,BrowserSourcemodels, andbrowser_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, 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 ce6f946. Summary will update on new commits.