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
9 changes: 9 additions & 0 deletions apps/templates/stagehand-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.env*
.eve
.vercel
.output
.nitro
dist
.DS_Store
*.tsbuildinfo
82 changes: 82 additions & 0 deletions apps/templates/stagehand-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Stagehand extension template

This package demonstrates an eve extension that imports the published Stagehand v4 SDK directly.
It contributes native `run`, `snapshot`, and `screenshot` tools without an MCP bridge or a copied
Playwright compatibility layer.

The extension declares `@browserbasehq/stagehand` and `@browserbasehq/sdk` in
`eve.extension.externalDependencies`. Stagehand loads browser-extension assets relative to its
installed package, and the Browserbase SDK provides a bounded release fallback if the browser
transport fails during initialization or cleanup. eve preserves both dependencies when it builds a
consuming agent.

The `run` callback receives Stagehand v4's `Page` and `BrowserContext` objects, not complete
Playwright objects. The bundled agent instructions enumerate the supported methods and warn models
not to guess Playwright-only helpers. Snapshot IDs are currently descriptive rather than actionable
selectors because this template does not retain Stagehand's snapshot lookup maps between tools.

## Use in an eve project

This is a private source template rather than a published package. Its source manifest intentionally
uses the eve monorepo's `workspace:` and `catalog:` dependency ranges. Consume it as a workspace
package, or pack it before installing it elsewhere so pnpm materializes concrete dependency
versions.

For an agent package named `my-agent` in the same pnpm workspace, add the template as a workspace
dependency:

```bash
pnpm --filter my-agent add "@eve-template/stagehand-extension@workspace:*"
```

To try it from a separate eve project, create a local package artifact from this repository and add
that artifact to the agent project:

```bash
mkdir -p /tmp/eve-stagehand-extension
pnpm --filter @eve-template/stagehand-extension build
pnpm --filter @eve-template/stagehand-extension pack \
--pack-destination /tmp/eve-stagehand-extension

cd /path/to/eve-agent
pnpm add /tmp/eve-stagehand-extension/eve-template-stagehand-extension-0.0.0.tgz
```

Mount it by creating `agent/extensions/browser.ts` in the consuming project:

```ts
export { default } from "@eve-template/stagehand-extension";
```

## Build and test

```bash
pnpm --filter @eve-template/stagehand-extension build
pnpm --filter @eve-template/stagehand-extension typecheck
pnpm --filter @eve-template/stagehand-extension test:unit
```

## Browser configuration

Set `BROWSERBASE_API_KEY` and optionally `BROWSERBASE_PROJECT_ID` to use Browserbase. Without an API
key, the extension launches a headed local browser. Set `STAGEHAND_BROWSER` to `local` or
`browserbase` to choose explicitly. `BROWSERBASE_API_URL` can override the Browserbase API endpoint
for both launch and release.

The three tools share one browser for the life of the eve process. Browserbase sessions do not use
keep-alive, and `run` code can call `close()` to make the host close both Stagehand and the browser.
The next tool call starts a fresh browser. Tool operations, health probes, and cleanup are bounded;
a hung browser call is detached and cannot permanently block the serialized operation queue. If
browser cleanup fails, the extension requests release through the Browserbase SDK and retries a
failed release before launching another browser. Model-visible cleanup errors use stable messages
without exposing SDK, transport, or session details.

## Code execution boundary

`run` compiles model-authored JavaScript in the Node.js host before sending the serializable callback
to Stagehand. The callback executes in Stagehand's browser extension, where it can use `page`,
`context`, `act`, `observe`, and `extract`. Calling `close()` sends a cleanup request back to the host.

Browserbase provides the recommended isolation boundary. The callback does not execute in eve's Node
process, but it is still powerful browser-side code and should not be treated as a sandbox for
hostile input.
3 changes: 3 additions & 0 deletions apps/templates/stagehand-extension/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineExtension } from "eve/extension";

export default defineExtension();
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Use the Stagehand tools to control one persistent browser. Operations are serialized, and browser
state persists across calls.

- Use `snapshot` to inspect the active page. Its accessibility IDs are descriptive, not selectors.
- Use `run` for navigation and multi-step browser operations. It accepts the body of an async
JavaScript function; write direct `await` statements and return a JSON-serializable result.
- Use `screenshot` when visual inspection is useful.

`run` provides `page`, `context`, `act`, `observe`, `extract`, and `close`. `page` is Stagehand v4's
`Page`, not a Playwright `Page`. Treat these method lists as allow-lists and do not guess Playwright
methods such as `getByRole`, `getByText`, `frameLocator`, or `keyboard`.

Supported page methods include `goto`, `reload`, `goBack`, `goForward`, `click`, `hover`, `scroll`,
`dragAndDrop`, `type`, `keyPress`, `evaluate`, `addInitScript`, `setExtraHTTPHeaders`,
`setViewportSize`, `waitForLoadState`, `waitForTimeout`, `waitForSelector`, `screenshot`, `snapshot`,
`tools`, `url`, `title`, `close`, and `locator`.

Supported locator methods include `click`, `hover`, `fill`, `count`, `isChecked`, `inputValue`,
`isVisible`, `innerText`, `innerHtml`, `textContent`, `scrollTo`, `centroid`, `highlight`,
`sendClickEvent`, `type`, `selectOption`, `setInputFiles`, `first`, and `nth`. Locators take CSS or
XPath selectors. For example:

```js
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
await page.locator("a").click();
return { title: await page.title(), url: await page.url() };
```

Use Stagehand's exact signatures: `page.setViewportSize(width, height)`, `page.keyPress(key)`, and
`locator.scrollTo(percent)`. A locator does not provide `press` or `keyPress`.

Supported context methods include `pages`, `newPage`, `activePage`, `setActivePage`,
`addInitScript`, `setExtraHTTPHeaders`, `getDomainPolicy`, `setDomainPolicy`, `cookies`,
`addCookies`, and `clearCookies`.

Await `page.url()`, `page.title()`, and every context method. Use `page.evaluate` for DOM queries or
attributes that the locator allow-list does not cover. Use `act`, `observe`, and `extract` for
AI-assisted operations. Do not import packages, access Node.js APIs, or launch another browser.

Call `close()` in the final `run` after collecting the result when the browser session is no longer
needed. It asks the host to release the owned browser after the callback returns.
88 changes: 88 additions & 0 deletions apps/templates/stagehand-extension/extension/lib/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { ExperimentalBatchCallback } from "@browserbasehq/stagehand";

import { stagehandSession, type StagehandSession } from "./session.js";

interface RunEnvelope {
value?: unknown;
closeRequested: boolean;
executionError?: {
name: string;
message: string;
stack?: string;
};
}

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor as new (
...args: string[]
) => ExperimentalBatchCallback<Record<string, never>, RunEnvelope>;

export function compileRunCallback(
code: string,
): ExperimentalBatchCallback<Record<string, never>, RunEnvelope> {
if (code.trim().length === 0) throw new TypeError("run code must not be empty");

return new AsyncFunction(
"batch",
"input",
`"use strict";
const { page, context, act, observe, extract } = batch;
let closeRequested = false;
const close = async () => { closeRequested = true; };
let value;
let executionError;
try {
value = await (async () => {
${code}
})();
} catch (error) {
executionError = {
name: typeof error?.name === "string" ? error.name : "Error",
message: typeof error?.message === "string" ? error.message : String(error),
};
if (typeof error?.stack === "string") executionError.stack = error.stack;
}
return { value, closeRequested, executionError };`,
);
}

export async function runStagehandCode(
code: string,
session: StagehandSession = stagehandSession,
): Promise<string> {
const callback = compileRunCallback(code);
const value = await session.run(async (resources) => {
const envelope = await resources.stagehand.experimentalBatch(callback, {}, { timeout: 60_000 });
let cleanupError: unknown;
if (envelope.closeRequested) {
try {
await session.close(resources);
} catch (error) {
cleanupError = error;
}
}

if (envelope.executionError) {
const error = new Error(envelope.executionError.message);
error.name = envelope.executionError.name;
if (envelope.executionError.stack) error.stack = envelope.executionError.stack;
if (cleanupError) {
throw new AggregateError([error, cleanupError], "Run failed and cleanup also failed.", {
cause: error,
});
}
throw error;
}
if (cleanupError) throw cleanupError;
return envelope.value;
});
return stringifyResult(value);
}

function stringifyResult(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2) ?? String(value);
} catch {
return String(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Browserbase from "@browserbasehq/sdk";

export interface BrowserbaseSessionRelease {
apiKey: string;
baseUrl?: string;
sessionId: string;
}

const BROWSERBASE_API_URL = "https://api.browserbase.com";
const SESSION_RELEASE_MAX_RETRIES = 2;
const SESSION_RELEASE_TIMEOUT_MS = 10_000;

export class BrowserbaseSessionReleaseError extends Error {
override readonly name = "BrowserbaseSessionReleaseError";

constructor() {
super("Failed to release the Browserbase session.");
}
}

export async function releaseBrowserbaseSession(session: BrowserbaseSessionRelease): Promise<void> {
const browserbase = new Browserbase({
apiKey: session.apiKey,
baseURL: (session.baseUrl ?? BROWSERBASE_API_URL).replace(/\/+$/u, ""),
maxRetries: SESSION_RELEASE_MAX_RETRIES,
timeout: SESSION_RELEASE_TIMEOUT_MS,
});
// The generated SDK currently interpolates path parameters without encoding
// them. Keep the server-issued ID confined to one URL path segment.
const sessionId = encodeURIComponent(session.sessionId);

try {
await browserbase.sessions.update(sessionId, { status: "REQUEST_RELEASE" });
return;
} catch {
// Verify the remote state below before reporting a failed retry.
}

try {
const remoteSession = await browserbase.sessions.retrieve(sessionId);
if (remoteSession.status === "COMPLETED") return;
} catch {
// Fall through to the stable lifecycle error below.
}

throw new BrowserbaseSessionReleaseError();
}
Loading