Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
extension: ${{ steps.filter.outputs.extension }}
integrations: ${{ steps.filter.outputs.integrations }}
is_internal_head: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
sdk-ts: ${{ steps.filter.outputs.sdk-ts }}
sdk-python: ${{ steps.filter.outputs.sdk-python }}
sdk-go: ${{ steps.filter.outputs.sdk-go }}
Expand All @@ -42,6 +44,8 @@ jobs:
- 'pnpm-lock.yaml'
- 'turbo.json'
- '.github/**'
integrations:
- 'packages/integrations/**'
sdk-python:
- 'packages/sdk-python/**'
- 'packages/extension/**'
Expand Down Expand Up @@ -167,6 +171,7 @@ jobs:
packages/extension/dist/**
packages/extension/artifacts/**
packages/sdk-ts/dist/**
packages/integrations/dist/**
packages/evals/dist/**
retention-days: 1

Expand All @@ -191,6 +196,21 @@ jobs:
env:
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}

integrations:
name: TypeScript integrations
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [build, determine-changes]
if: needs.determine-changes.outputs.integrations == 'true'
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: ./.github/actions/setup-node-pnpm
with:
use-prebuilt-artifacts: "true"

- run: pnpm exec turbo run test:unit --filter @browserbasehq/stagehand-integrations

browser-ts:
name: TypeScript browser
runs-on: ubuntu-latest
Expand All @@ -211,6 +231,7 @@ jobs:
env:
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
- name: Browserbase smoke test
if: needs.determine-changes.outputs.is_internal_head == 'true'
run: >-
pnpm exec vitest run --root .
packages/sdk-ts/tests/browser-runtime/stagehandBrowserbaseSmoke.test.ts
Expand Down
71 changes: 71 additions & 0 deletions packages/integrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Stagehand integrations

This package contains shared integration surfaces for Stagehand V4. It is private while the public API and packaging contract are validated.

## Code mode

The `./codemode` export gives an agent one `code_execute` tool backed by a persistent Stagehand browser. Frameworks can either launch the thin local MCP server or wrap `StagehandCodeExecutor` as a native tool.

```ts
import {
StagehandCodeExecutor,
stagehandCodeConfigFromEnv,
} from "@browserbasehq/stagehand-integrations/codemode";

const executor = new StagehandCodeExecutor(stagehandCodeConfigFromEnv());

try {
const result = await executor.execute({
code: `
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
return { title: await page.title(), url: await page.url() };
`,
});
console.log(result);
} finally {
await executor.close();
}
```

The executor initializes the browser on the first valid call, serializes calls, and preserves pages, cookies, and navigation state until its owner closes it.

### Low-level eval integration

Eval harnesses that already own Stagehand and browser initialization should call `executeStagehandSnippet` directly. This reuses the exact generated-code semantics without replacing the eval harness's startup, cleanup, task bindings, or metrics collection.

### Local MCP integration

The `./codemode/stdio-server` export is an internal process entrypoint. It is not a command-line interface and accepts no arguments. The owning framework launches one process per agent run and selects local or Browserbase startup through its environment:

```text
STAGEHAND_BROWSER=local
STAGEHAND_BROWSER=browserbase
```

The process stays alive across calls and closes when its input stream ends. `SIGINT` and `SIGTERM` perform bounded graceful cleanup and preserve signal-style exit codes. If generated JavaScript blocks the JavaScript event loop, the server cannot run its cleanup handlers. The owner must terminate the entire process tree, escalate to `SIGKILL` after its own deadline, and start a new process before accepting more work. Killing only the Node process can leave its local browser child alive.

### Configuration

`stagehandCodeConfigFromEnv()` recognizes:

| Variable | Purpose |
| ------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `STAGEHAND_BROWSER` | Optional `local` or `browserbase` override |
| `BROWSERBASE_API_KEY` | Selects and authenticates Browserbase when present |
| `BROWSERBASE_PROJECT_ID` | Optional Browserbase project forwarded when creating a session |
| `STAGEHAND_MODEL_NAME` | Optional Stagehand model name |
| `STAGEHAND_MODEL_API_KEY` | Optional explicit model-provider key |
| Provider API keys | Supplies the key for a matching explicit model provider |
| `GEMINI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY` | Selects `google/gemini-2.5-flash-lite` when no model is explicit |

Without a browser override, the helper selects Browserbase when `BROWSERBASE_API_KEY` exists and a headless local browser otherwise.

Native callers run generated JavaScript in their own process. An `AbortSignal` can cancel queued work before the snippet begins, but it cannot safely preempt arbitrary JavaScript already running in the same process. Native integrations that require hard time limits should put the executor behind a child-process boundary, as the stdio MCP integration does.

### Skill and reference

`codemode/SKILL.md` is the concise agent guide. `codemode/REFERENCE.md` is the longer API lookup. Both are exported as raw package assets and as bundle-safe JavaScript strings.

### Security boundary

The code-mode executor does not provide a sandbox. Generated JavaScript runs in the host process and inherits that process's filesystem, network, and environment access. A framework may place the tool inside its own sandbox, container, or other isolation boundary.
211 changes: 211 additions & 0 deletions packages/integrations/codemode/REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Stagehand V4 code-mode reference

This reference describes the Stagehand objects injected into `code_execute`. `SKILL.md` contains the
short operational guide intended for every agent context; this file is the longer lookup reference.

## Function body contract

The tool builds an async function from the submitted body. These values are injected:

```ts
page: Page;
context: BrowserContext;
stagehand: Stagehand;
z: typeof import("zod/v4");
console: Pick<Console, "log" | "warn" | "error">;
```

Eval harnesses may add JSON-safe bindings such as `startUrl` and `task`. The deterministic eval arm
intentionally omits `stagehand` and `z`.

## Page

Navigation and page state:

```ts
page.goto(url, { waitUntil?, timeout? }): Promise<Response | null>
page.reload(options?): Promise<Response | null>
page.goBack(options?): Promise<Response | null>
page.goForward(options?): Promise<Response | null>
page.url(): Promise<string>
page.title(): Promise<string>
page.pageId: string
page.close(): Promise<void>
```

Input and pointer operations:

```ts
page.click(x, y, options?): Promise<void>
page.hover(x, y): Promise<void>
page.scroll(x, y, deltaX, deltaY): Promise<void>
page.dragAndDrop(fromX, fromY, toX, toY, options?): Promise<void>
page.type(text, options?): Promise<void>
page.keyPress(key, options?): Promise<void>
```

DOM and setup operations:

```ts
page.evaluate(expressionOrFunction, arg?): Promise<unknown>
page.addInitScript(script, arg?): Promise<void>
page.setExtraHTTPHeaders(headers): Promise<void>
page.setViewportSize(width, height, options?): Promise<void>
page.waitForLoadState('load' | 'domcontentloaded' | 'networkidle', timeout?): Promise<void>
page.waitForTimeout(milliseconds): Promise<void>
page.waitForSelector(selector, options?): Promise<boolean>
page.screenshot(options?): Promise<Buffer>
page.snapshot({ includeIframes? }): Promise<{ formattedTree; xpathMap; urlMap }>
page.locator(selector): Locator
```

`page.evaluate` accepts a function and one serializable argument. Prefer one object when several
values are needed:

```js
return await page.evaluate(
({ selector, limit }) =>
Array.from(document.querySelectorAll(selector))
.slice(0, limit)
.map((node) => node.textContent?.trim() ?? ""),
{ selector: "article h2", limit: 20 },
);
```

## Locator

```ts
locator.click(options?): Promise<void>
locator.hover(): Promise<void>
locator.fill(value): Promise<void>
locator.count(): Promise<number>
locator.isChecked(): Promise<boolean>
locator.inputValue(): Promise<string>
locator.isVisible(): Promise<boolean>
locator.innerText(): Promise<string>
locator.innerHtml(): Promise<string>
locator.textContent(): Promise<string>
locator.scrollTo(percent): Promise<void>
locator.centroid(): Promise<{ x: number; y: number }>
locator.highlight(options?): Promise<void>
locator.sendClickEvent(options?): Promise<void>
locator.type(text, options?): Promise<void>
locator.selectOption(values): Promise<string[]>
locator.setInputFiles(files): Promise<void>
locator.first(): Locator
locator.nth(index): Locator
```

Stagehand locators do not implement Playwright's collection/filter/frame helpers. Use `count` with
`nth`, or use `page.evaluate` for bulk DOM reads.

## BrowserContext

```ts
context.pages(): Promise<Page[]>
context.newPage(options?): Promise<Page>
context.activePage(): Promise<Page | undefined>
context.setActivePage(page): Promise<void>
context.setExtraHTTPHeaders(headers): Promise<void>
context.getDomainPolicy(): Promise<DomainPolicy | null>
context.setDomainPolicy(policy: DomainPolicy | null): Promise<void>
context.cookies(urls?): Promise<Cookie[]>
context.addCookies(cookies): Promise<void>
context.clearCookies(options?): Promise<void>
```

The tool owner closes the context and browser. Generated code should not call `context.close()`,
`stagehand.close()`, or `stagehand.browser.close()`.

## Stagehand AI methods

```ts
stagehand.act(instruction, options?): Promise<{
data: { success; message; actionDescription; actions };
metadata: StagehandResultMetadata;
}>

stagehand.observe(instruction?, options?): Promise<{
data: Action[];
metadata: StagehandResultMetadata;
}>

stagehand.extract(instruction, schema?, options?): Promise<{
data: unknown;
metadata: StagehandResultMetadata;
}>
```

The useful operation result is under `.data`. The `.metadata` object contains cache and model-usage
information for that operation.

To target a non-active page:

```js
const pages = await context.pages();
const result = await stagehand.extract("Extract the heading", z.object({ heading: z.string() }), {
page: pages[1],
});
return result.data;
```

## Multiple pages

Page arrays are snapshots. Await `context.pages()` again after an interaction that may open or
close a tab. Use `context.setActivePage(page)` when later operations should target that tab.

```js
const before = await context.pages();
const beforePageIds = new Set(before.map((page) => page.pageId));
await before[0].locator('a[target="_blank"]').first().click();
await before[0].waitForTimeout(500);
const after = await context.pages();
const opened = after.find((candidate) => !beforePageIds.has(candidate.pageId));
if (!opened) throw new Error("Expected a new page");
await context.setActivePage(opened);
return await opened.url();
```

## Pagination pattern

```js
const records = new Map();
const seenPages = new Set();

for (let pageIndex = 0; pageIndex < 50; pageIndex++) {
const rows = await page.evaluate(() =>
Array.from(document.querySelectorAll("table tbody tr")).map((row) =>
Array.from(row.querySelectorAll("td")).map((cell) => cell.textContent?.trim() ?? ""),
),
);
const signature = JSON.stringify(rows);
if (seenPages.has(signature)) break;
seenPages.add(signature);
for (const row of rows) records.set(JSON.stringify(row), row);

const next = page.locator(".paginate_button.next").first();
if (!(await next.isVisible())) break;
await next.click();
await page.waitForTimeout(300);
}

return Array.from(records.values());
```

## Common incompatibilities

| Incorrect assumption | Stagehand V4 form |
| ------------------------------ | ----------------------------------------------------------- |
| `page.url` is a string | `await page.url()` |
| `page.content()` | `await page.locator('body').innerHtml()` or `page.evaluate` |
| `locator.innerHTML()` | `await locator.innerHtml()` |
| `locator.all()` | `count()` plus `nth(index)` |
| `locator.evaluate()` | `page.evaluate()` |
| `page.frameLocator()` | `page.snapshot({ includeIframes: true })` plus `xpathMap` |
| `context.waitForEvent('page')` | Compare `await context.pages()` before and after |
| `result.success` from `act` | `result.data.success` |
| Raw array from `observe` | `result.data` |
| Raw object from `extract` | `result.data` |

When this reference and the installed SDK disagree, the installed `@browserbasehq/stagehand`
TypeScript declaration files are authoritative.
Loading
Loading