Skip to content

Remove the legacy Go Stagehand lifecycle - #2549

Merged
miguelg719 merged 10 commits into
feat/stagehand-go-browser-factoriesfrom
feat/stagehand-go-remove-legacy-lifecycle
Aug 3, 2026
Merged

Remove the legacy Go Stagehand lifecycle#2549
miguelg719 merged 10 commits into
feat/stagehand-go-browser-factoriesfrom
feat/stagehand-go-remove-legacy-lifecycle

Conversation

@miguelg719

@miguelg719 miguelg719 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Completes the Go port of the browser-lifecycle stack (#2517#2523) by making stagehand.Create(ctx, CreateOptions) the sole construction path, mirroring the TypeScript end state.

  • removes New, Init, StagehandClientInitParams, the BrowserSource union, and the public ResolvedBrowserSource; the raw-CDP-with-headers path has no replacement (ConnectLocalBrowser takes a bare CDP URL)
  • Browser() returns the exact handle passed to CreateOptions
  • Stagehand.Close never closes the CDP transport, Chrome process, or Browserbase session; browser lifetime is exclusively Browser.Close(ctx); Close results are memoized for TS closePromise parity
  • updates ErrNotInitialized message to point at the new lifecycle (exported var name unchanged)
  • migrates all 7 examples and the live tests to launch → Createclient.Close(ctx)browser.Close(ctx) (deferred in that order so a failed client close can't leak the process)
  • updates the Go ast-grep example-parity patterns to the multi-value stagehand.Create shape in the same commit (they gate TS CI)

Reviewer focus

  1. Stagehand.Close stops the runtime; Browser.Close owns browser/session cleanup — under no configuration does Stagehand touch the browser-owned transport.
  2. Browser acquisition options stay client-side; stagehand.init wire payload is unchanged (models.gen.go untouched, no regeneration).
  3. Parity accessors remain exactly {Browser, Context, Initialized}; the central StagehandInitParams literal stays in stagehand.go.

Follow-up (not in this PR)

  • packages/docs/v4/** Go snippets still show the deleted New/Init lifecycle (~10 files); docs migration should follow once this stack settles.

Stack

Verification

  • full package gates green: gofmt, go vet, go build, go test, generator --check + generator tests, all 7 examples compile
  • root pnpm run test:unit green (ast-grep example-parity + sdk-parity against the migrated Go examples/source)
  • changeset check passed; live tests (CHROME_PATH) migrated and exercised by CI

Summary by cubic

Make stagehand.Create(ctx, CreateOptions) the only way to build the Go client and finalize split lifecycles: the client stops the worker; the Browser owns its cleanup. Stagehand.Close memoizes the first result (including failures) for repeated or concurrent calls.

  • Refactors

    • Removed New, Init, StagehandClientInitParams, the BrowserSource union, and public ResolvedBrowserSource; deleted the legacy resolver and tests.
    • Stagehand.Browser() returns the exact *Browser passed to Create; the client never closes Chrome or a Browserbase session.
    • Standardized factories: LaunchLocalBrowser, ConnectLocalBrowser, LaunchBrowserbase (uses BrowserbaseLaunchOptions); factories materialize the bundled extension and set extensionDir on Browser.
    • Enforced local KeepAlive at the factory: Browser.Close terminates a launched Chrome only when KeepAlive is false (covered by tests).
    • Updated examples, live tests, and ast-grep rules to launch/connect → Createclient.Close(ctx)browser.Close(ctx); Go parity now uses create() (Python still init()).
  • Migration

    • Replace:
      • client := stagehand.New(...); client.Init(ctx) with:
        • browser := stagehand.LaunchLocalBrowser(...) | ConnectLocalBrowser(...) | LaunchBrowserbase(...)
        • client, _ := stagehand.Create(ctx, stagehand.CreateOptions{Browser: browser, ...})
    • Manage lifetimes separately: defer client.Close(ctx) and defer browser.Close(ctx).
    • For existing CDP, use ConnectLocalBrowser(ctx, LocalBrowserConnectOptions{CDPURL: ...}).
    • For Browserbase, create via LaunchBrowserbase(ctx, stagehand.BrowserbaseLaunchOptions{APIKey: ...}).

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

Review in cubic

Review finding: after a failed first Close, every later caller got nil,
hiding the cleanup failure and diverging from the TS memoized closePromise
(and from Browser.Close in this SDK). Close now records its joined result
and repeated/concurrent calls return it without re-running teardown.
@changeset-bot

changeset-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ad12189

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 06:09

@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.

4 issues found across 26 files

Confidence score: 3/5

  • In packages/sdk-go/browserbase_session.go (LaunchBrowserbase), allowing caller-supplied extension IDs can bypass the Stagehand extension even though the factory reports it as preloaded, which can cause runtime feature failures and hard-to-diagnose behavior mismatches — reject both extension-ID inputs at the factory boundary and remove those fields from launch options.
  • In packages/sdk-go/stagehand.go (newStagehandWithClient), running the full createWithAdapters path and issuing init via context.Background() means init calls may hang without timeout/cancellation, increasing risk of stuck startup and leaked work under failure conditions — thread through caller context (or a bounded timeout) for the init RPC.
  • In packages/sdk-go/chrome_launcher.go, the KeepAlive ownership shift is under-tested, so close semantics may regress and either terminate Chrome unexpectedly or leak processes depending on KeepAlive state — add focused true/false regression tests with a closable mock source to verify Browser.Close behavior.
  • In packages/sdk-go/browser_factories.go, materializeBrowserExtension is now a pass-through alias, which adds indirection without behavior and can obscure where extension resolution dependencies were removed — either inline/remove the alias or restore meaningful separation with explicit dependency handling.
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-go/browserbase_session.go">

<violation number="1" location="packages/sdk-go/browserbase_session.go:196">
P2: `LaunchBrowserbase` still accepts caller-provided extension IDs, so callers can launch without the Stagehand extension while the factory marks it preloaded; reject both extension fields at the factory boundary and remove them from the public launch shape. This conflicts with the factory schema contract and can leave the returned Browser unable to find its Stagehand service worker.

(Based on your team's feedback about Browserbase launch schema boundary.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/sdk-go/chrome_launcher.go">

<violation number="1" location="packages/sdk-go/chrome_launcher.go:95">
P3: Local-browser `KeepAlive` ownership moved to the factory but has no regression coverage. Add focused true/false tests with a closable mocked source so `Browser.Close` is verified to retain or terminate launched Chrome as configured.

(Based on your team's feedback about adding unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/sdk-go/browser_factories.go">

<violation number="1" location="packages/sdk-go/browser_factories.go:248">
P3: materializeBrowserExtension is now a pointless one-line alias for materializeStagehandExtension with an identical signature and no transformation. Since this change removed the previous browserSourceResolverDependencies adaptation, the indirection no longer serves a purpose and adds a small amount of confusion about which function actually owns the logic. Consider calling materializeStagehandExtension directly from the two call sites (launchLocalBrowserWithDependencies and connectLocalBrowserWithDependencies) and dropping the wrapper, or keeping only one named entry point.</violation>
</file>

<file name="packages/sdk-go/stagehand.go">

<violation number="1" location="packages/sdk-go/stagehand.go:463">
P3: The `newStagehandWithClient` helper was rewritten to run the entire `createWithAdapters` flow, and it calls the init RPC with `context.Background()` (no timeout, no cancellation). It lives in the production `stagehand.go` yet is only ever referenced from `_test.go` files. Since it drives `stagehand.init` through the real create path, an unbounded background context means a hung/blocked transport would stall forever with no way to cancel. Consider moving it into a `_test.go` file and passing a caller-supplied context through to `createWithAdapters` to avoid the footgun in production code.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant App as Application Code
    participant Factory as Browser Factory
    participant Stagehand as Stagehand.Create()
    participant RPC as CDP Transport
    participant Worker as Stagehand Worker
    
    Note over App,Worker: NEW: Two-phase lifecycle (Browser then Stagehand)
    
    App->>Factory: LaunchLocalBrowser/ConnectLocalBrowser/LaunchBrowserbase(ctx, opts)
    Factory->>Factory: Resolve browser source (launch Chrome / connect CDP / create Browserbase session)
    Factory->>Factory: Materialize Stagehand extension
    Factory-->>App: *Browser handle
    
    App->>Stagehand: Create(ctx, CreateOptions{Browser: browser, ...})
    Stagehand->>RPC: Connect via claimed browser CDP
    RPC-->>Stagehand: protocolClient
    Stagehand->>RPC: onNotification("stagehand.log")
    Stagehand->>RPC: onRequest("llm.generate") [optional]
    Stagehand->>Worker: stagehand.init(StagehandInitParams)
    Worker-->>Stagehand: StagehandInitResult{Initialized: true}
    Stagehand-->>App: *Stagehand (initialized)
    
    Note over App,Stagehand: Operations use Stagehand's rpc/context
    App->>Stagehand: Act/Extract/Observe/Context()
    Stagehand->>RPC: RPC calls via CDP
    RPC-->>Stagehand: Results
    Stagehand-->>App: Results
    
    Note over App,Worker: CHANGED: Close only stops runtime, not browser
    
    App->>Stagehand: Close(ctx)
    Stagehand->>Stagehand: Check closed flag (memoized)
    alt First close
        Stagehand->>RPC: stagehand.close
        RPC-->>Stagehand: Result (or ErrCDPConnectionClosed)
        Stagehand->>Stagehand: Remove LLM handler
        Stagehand->>Stagehand: Remove notification handler
        Stagehand->>RPC: close() transport
        Stagehand->>Stagehand: Set initialized=false, closed=true
        Stagehand-->>App: errors.Join(closeErr, rpcErr) [memoized]
    else Subsequent close
        Stagehand-->>App: memoized closeResult
    end
    
    Note over App,Factory: Browser lifetime is always explicit
    
    App->>Factory: browser.Close(ctx) [deferred after client.Close]
    alt Local Browser
        Factory->>Factory: Kill Chrome process
        Factory->>Factory: Remove extension directory
    else Browserbase Session
        Factory->>Factory: Release session
    else Existing CDP
        Factory->>Factory: Close CDP client (no transport ownership)
    end
    Factory-->>App: nil or error
    
    alt Error: client.Close fails before browser.Close
        Note over App,Factory: Deferred browser.Close still runs (errors.Join)
        App->>Factory: browser.Close(ctx)
    end
Loading

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

Re-trigger cubic

Comment thread packages/sdk-go/browserbase_session.go
Comment thread packages/sdk-go/chrome_launcher.go
Comment thread packages/sdk-go/browser_factories.go Outdated
Comment thread packages/sdk-go/stagehand.go Outdated
…d-go-remove-legacy-lifecycle

# Conflicts:
#	packages/sdk-go/browser_factories.go
#	packages/sdk-go/browserbase_session_test.go
#	packages/sdk-go/stagehand.go
Call materializeStagehandExtension directly now that the wrapper no longer
adapts anything, move the newStagehandWithClient test helper out of the
production client into client_test.go, and add a factory-level regression test
asserting Browser.Close terminates a launched local browser only when
KeepAlive is false.
…lifecycle' into feat/stagehand-go-remove-legacy-lifecycle
Comment thread rules/ast-grep/example-parity.test.ts
…d-go-remove-legacy-lifecycle

# Conflicts:
#	packages/sdk-go/examples/custom-logging.go
…d-go-remove-legacy-lifecycle

Migrate the model-gateway example onto the Browser handle lifecycle, mirroring
packages/sdk-ts/examples/model-gateway.ts: launch the Browserbase browser with
LaunchBrowserbase and attach with Create, so the Browserbase API key travels on
the handle instead of StagehandClientInitParams.
@miguelg719
miguelg719 merged commit e37188f into v4-spike Aug 3, 2026
22 checks passed
miguelg719 added a commit that referenced this pull request Aug 3, 2026
# why

Ports the merged TypeScript browser-lifecycle stack (#2517#2523) to the
Go SDK. This bottom PR lands the internal plumbing the factory lifecycle
needs — borrow-don't-own transports and Browserbase session connect
semantics — with zero public API change.

# what changed

- adds transport ownership to `rpcClient`: shutdown still cancels,
rejects pending calls, and clears handlers, but only closes the
transport when owned
- adds `retrieveSession` (GET `/v1/sessions/{id}`) to the Browserbase
client with deliberately lenient validation (`id` required;
`connectUrl`/`region` optional)
- adds internal `connectSession` that never takes release ownership of
an existing session
- caller-supplied extension IDs suppress Stagehand extension
provisioning **and** cleanup on every path (success, create failure,
close)

# intentionally not included

- no exported identifier added or changed; legacy `New`/`Init`/`Close`
behavior is unchanged
- no factories, no `Browser` handle, no `Create`

# stack

1. **this PR — transport and Browserbase session foundations**
2. #2548 — browser factories and `stagehand.Create`
3. #2549 — remove the legacy lifecycle and migrate consumers

# test plan

- table tests for un-owned shutdown (transport left open, pending calls
still rejected), `httptest` coverage for `retrieveSession`, fake-API
cases proving caller extension IDs are never uploaded/deleted
- full package gates green: gofmt, `go vet`, `go build`, `go test`,
generator `--check`, examples compile, root `pnpm run test:unit`,
changeset check
miguelg719 added a commit that referenced this pull request Aug 3, 2026
# why

With transport ownership and session foundations in place, this stack
entry adds the full new lifecycle additively: browser factories and
`stagehand.Create`, while legacy `New`/`Init` keeps working untouched.

# what changed

- adds exported `Browser` handle (unexported fields;
`Provider`/`Origin`/`Closed`/`Close` only) with one-time Stagehand
claiming and idempotent, memoized, race-safe `Close`
- adds `LaunchLocalBrowser` / `ConnectLocalBrowser` /
`LaunchBrowserbase` / `ConnectBrowserbase`, each resolving only after
the Stagehand extension service worker is ready
- implements the ownership rule `ownsSource = launched && !keepAlive`;
failed connects clean up owned sources with `errors.Join`, keep-alive
sources are left running
- implements factory-path downloads via root-session
`Browser.setDownloadBehavior`
- Browserbase launches merge `userMetadata` with
`stagehand_sdk_language: "go"` and honor caller extension IDs per the
foundations PR
- adds `stagehand.Create(ctx, CreateOptions)`: claims the handle,
attaches over the browser-owned transport (never closes it), releases
the claim on failure so `Create` can be retried on the same handle
- keeps the central `StagehandInitParams` literal in `stagehand.go`
feeding both lifecycles (ast-grep sdk-parity constraint)

# compatibility

- legacy `New`/`Init`/`Close`, all examples, and the ast-grep rules pass
unchanged — this layer is purely additive

# stack

1. #2547 — transport and Browserbase session foundations
2. **this PR — browser factories and `stagehand.Create`**
3. #2549 — remove the legacy lifecycle and migrate consumers

# test plan

- new `browser_test.go`: claim/release/re-claim, close idempotence and
concurrent-close context handling, ownership matrix, extension routing,
download validation and command capture, metadata/region propagation
- `Create` wire-shape tests over a recording protocol client: handle API
key wins, local handles omit `Browser`, failed init releases the claim
- full package gates green: gofmt, `go vet`, `go build`, `go test`,
generator `--check`, examples compile, root `pnpm run test:unit`,
changeset check
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.

2 participants