Skip to content

feat: add Stagehand code execution tool - #2619

Open
shrey150 wants to merge 7 commits into
shrey/stg-2765-codemode-packagefrom
shrey/stg-2765-codemode-code-tool
Open

feat: add Stagehand code execution tool#2619
shrey150 wants to merge 7 commits into
shrey/stg-2765-codemode-packagefrom
shrey/stg-2765-codemode-code-tool

Conversation

@shrey150

@shrey150 shrey150 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Why

This second stack layer adds the independently usable code-execution product core on top of the package and MCP host from #2597. Keeping execution separate from generated agent guidance lets reviewers focus on browser ownership, configuration, schemas, queueing, redaction, and lifecycle behavior.

Stack

  1. feat: scaffold Stagehand code-mode MCP host #2597 — private package, MCP stdio host, lifecycle, repository build/test wiring
  2. This PRcode_execute, Stagehand executor, local/Browserbase configuration, schemas, and runtime tests
  3. feat: add Stagehand code-mode guidance #2620SKILL.md, REFERENCE.md, generated exports, package assets, and guidance loading checks
  4. #2626 — Vercel AI SDK MCP example and smoke flows
  5. #2627 — Mastra MCP example and smoke flows
  6. #2628 — CrewAI MCP example and smoke flows
  7. #2629 — LangChain Deep Agents MCP example and smoke flows

What changed

  • adds StagehandCodeExecutor with lazy browser startup and one long-lived session per executor
  • serializes calls in first-in, first-out order so browser mutations do not race
  • supports native integration and exactly one MCP tool, code_execute
  • injects page, context, stagehand, Zod, and a bounded console into async JavaScript snippets
  • validates input and structured output with explicit success/failure schemas
  • bounds returned values and logs on valid UTF-8 character boundaries and bounds error messages
  • redacts configured secrets, credentials, bearer tokens, and URLs from returned errors
  • closes partially initialized browsers, emits typed sanitized lifecycle errors, and drains queued work before normal cleanup

Local and remote startup

The stdio process reads startup configuration from its environment:

Setting Behavior
STAGEHAND_BROWSER=local Starts a headless local browser, even if Browserbase credentials are present.
STAGEHAND_BROWSER=browserbase Starts a Browserbase browser and requires BROWSERBASE_API_KEY.
no explicit setting Selects Browserbase when its API key exists and local mode otherwise.

BROWSERBASE_PROJECT_ID is forwarded when present. Explicit model names select only their matching provider key, and Google-key precedence matches the eval-native configuration.

Timeout and cancellation boundary

An abort signal can cancel queued work before its snippet begins. Arbitrary JavaScript already executing in-process cannot be safely preempted. If code blocks the Node event loop, the owning framework must terminate the entire child process tree, escalate to SIGKILL after its deadline, and create a replacement process. Killing only the Node process can leave a local browser descendant alive.

Intentionally not included

  • no SKILL.md or REFERENCE.md
  • no generated prompt constants or package asset exports
  • no framework-specific watchdog or consumer adapter
  • no published package surface; the package remains private

E2E Test Matrix

Command / flow Observed output Confidence / sufficiency
pnpm --filter @browserbasehq/stagehand-integrations typecheck && pnpm --filter @browserbasehq/stagehand-integrations test Typecheck and build passed; 8 test files and 63 tests passed. Covers configuration, project forwarding, model selection, schemas, snippet bindings, queueing, cancellation, cleanup, redaction, MCP registration, and compiled stdio lifecycle.
Native executor, local browser, two sequential calls PASS in 2.1 seconds; two tabs were created and the second call observed the same active page and both URLs. Proves the local native build starts a real browser and persists state across calls.
Compiled stdio MCP, local browser, two sequential calls PASS in 2.3 seconds; exactly code_execute was discovered and state persisted across both calls. Proves the local process transport, tool registration, real browser startup, and session reuse.
Compiled stdio MCP, Browserbase browser, two sequential calls PASS in 8.5 seconds; exactly code_execute was discovered and both remote pages persisted. Proves the remote startup option and real Browserbase session path.
Owner-enforced hung-process recovery The default transport ended the blocked Node child but required owner cleanup for 8 local-browser descendants; owner cleanup terminated them and a replacement child navigated successfully. Confirms the documented process-tree ownership requirement and successful replacement behavior; the package itself does not provide hard preemption.
pnpm exec turbo run fmt:check lint typecheck --concurrency=1 9/9 repository tasks passed. Supports repository-wide formatting, lint, and type compatibility while avoiding an unrelated generated-protocol formatting race in the parallel local command.

Changeset

None. This changes a private workspace package and does not publish a release.

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a70af28

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

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

All reported issues were addressed across 18 files

Architecture diagram
sequenceDiagram
    participant Host as Agent Framework / Host
    participant Exec as StagehandCodeExecutor
    participant Queue as Serial Queue
    participant Snip as executeStagehandSnippet
    participant Stagehand as Stagehand Instance
    participant Browser as Browser (local/Browserbase)
    participant Page as Page
    participant MCP as MCP Server / Tool
    participant Config as stagehandCodeConfigFromEnv

    Note over Host,Config: Startup: Configuration and Browser Selection
    Host->>Config: Read environment variables
    Config->>Config: Determine browser type (local/browserbase)
    alt STAGEHAND_BROWSER=local
        Config->>Config: Set local headless browser
    else STAGEHAND_BROWSER=browserbase
        Config->>Config: Validate BROWSERBASE_API_KEY present
        Config->>Config: Forward project ID if set
    else No explicit setting
        alt BROWSERBASE_API_KEY exists
            Config->>Config: Select Browserbase
        else
            Config->>Config: Select local headless
        end
    end
    Config->>Config: Resolve model name and API key
    alt Explicit STAGEHAND_MODEL_NAME
        Config->>Config: Use provider-specific API key
        alt Provider is Anthropic
            Config->>Config: Add dangerous-direct-browser-access header
        end
    else No model name, Google key present
        Config->>Config: Default to google/gemini-2.5-flash-lite
    end
    Config-->>Host: Return StagehandCodeConfig

    Note over Host,MCP: Execution Flow
    Host->>Exec: new StagehandCodeExecutor(config)
    Host->>Exec: execute({ code }, signal?)

    Note over Exec,Queue: Serialization and Validation
    Exec->>Exec: validate input (size, non-empty)
    alt Invalid input
        Exec-->>Host: Return failure with kind="validation"
    else Valid input
        Exec->>Queue: Chain onto FIFO queue
        Queue-->>Exec: Wait for previous operations
    end

    Note over Exec,Stagehand: Lazy Browser Initialization
    Exec->>Exec: ensureStagehand()
    alt Stagehand not yet created
        Exec->>Browser: Launch (local or browserbase)
        alt Launch successful
            Exec->>Stagehand: Stagehand.create(browser, config)
            alt Stagehand create fails
                Exec->>Browser: Close browser
                Exec-->>Host: Return failure with kind="runtime"
            end
        else Launch fails
            Exec-->>Host: Return failure with kind="runtime"
        end
    end
    Exec-->>Exec: Stagehand instance ready

    Note over Exec,Page: Snippet Execution
    Exec->>Page: Get active page (or first/new)
    alt Signal aborted before execution
        Exec-->>Host: Return failure with kind="aborted"
    else Signal not aborted
        Exec->>Snip: executeStagehandSnippet({ code, page, context, stagehand, console })
        Snip->>Snip: Create AsyncFunction with bindings
        Note over Snip: Injects page, context, stagehand, z (Zod), console
        Snip->>Page: Execute snippet code
        alt Snippet succeeds
            Page-->>Snip: Return value
            Snip-->>Exec: Return value
            Exec->>Page: Read page state (URL, title)
            Exec-->>Host: Return success with page state and value
        else Snippet throws
            Page-->>Snip: Throw error
            Snip-->>Exec: Throw error
            Exec->>Exec: normalizeError (redact secrets)
            Exec-->>Host: Return failure with kind="runtime"
        end
    end

    Note over Exec: Cleanup and Shutdown
    Host->>Exec: close()
    Exec->>Exec: Drain queued operations
    Exec->>Stagehand: stagehand.close()
    Exec->>Browser: browser.close()
    alt Both close fail
        Exec-->>Host: AggregateError
    end
    Exec-->>Host: Cleanup complete

    Note over MCP,MCP: MCP Tool Registration
    Host->>MCP: createCodeModeMcpServer(executor)
    MCP->>MCP: registerTool("code_execute", schema, handler)
    MCP-->>Host: McpServer ready
    Host->>MCP: connect to stdio transport
    Note over MCP,Host: Tool calls flow through MCP protocol
    Host->>MCP: callTool("code_execute", { code })
    MCP->>Exec: executor.execute(input, signal)
    Exec-->>MCP: result
    MCP->>MCP: Format as text + structured content
    alt result.ok
        MCP-->>Host: isError=false, content=JSON
    else
        MCP-->>Host: isError=true, content=JSON
    end
Loading

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

Re-trigger cubic

Comment thread packages/integrations/src/codemode/executor.ts Outdated
Comment thread packages/integrations/src/codemode/executor.ts Outdated
Comment thread packages/integrations/src/codemode/index.ts Outdated
Comment thread packages/integrations/tests/stdio-server.test.ts
Comment thread packages/integrations/src/codemode/config.ts Outdated
Comment thread packages/integrations/src/codemode/config.ts Outdated
Comment thread packages/integrations/src/codemode/index.ts
Comment thread packages/integrations/src/codemode/tool-contract.ts Outdated
Comment thread packages/integrations/src/codemode/executor.ts Outdated

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

All reported issues were addressed across 8 files (changes from recent commits).

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

Re-trigger cubic

Comment thread packages/integrations/src/codemode/executor.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant