From ec7b841964ca48552dce8a69fe0beb9e76782a9f Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Mon, 20 Jul 2026 14:28:12 -0700 Subject: [PATCH 1/8] Add Browserbase Eve extension --- examples/integrations/vercel/README.md | 7 +- .../vercel/eve-example/.env.example | 2 + .../vercel/eve-example/.gitignore | 9 ++ .../integrations/vercel/eve-example/.nvmrc | 1 + .../integrations/vercel/eve-example/README.md | 48 ++++++++ .../vercel/eve-example/agent/agent.ts | 5 + .../agent/extensions/browserbase.ts | 6 + .../vercel/eve-example/agent/instructions.md | 19 +++ .../vercel/eve-example/package.json | 29 +++++ .../vercel/eve-example/tsconfig.json | 13 ++ examples/integrations/vercel/eve/.gitignore | 8 ++ examples/integrations/vercel/eve/.nvmrc | 1 + examples/integrations/vercel/eve/README.md | 114 ++++++++++++++++++ .../vercel/eve/extension/extension.ts | 11 ++ .../eve/extension/instructions/browser.md | 7 ++ .../vercel/eve/extension/lib/browserbase.ts | 7 ++ .../vercel/eve/extension/lib/session-state.ts | 11 ++ .../vercel/eve/extension/lib/stagehand.ts | 103 ++++++++++++++++ .../vercel/eve/extension/tools/act.ts | 20 +++ .../vercel/eve/extension/tools/agent.ts | 22 ++++ .../vercel/eve/extension/tools/extract.ts | 23 ++++ .../vercel/eve/extension/tools/fetch.ts | 61 ++++++++++ .../vercel/eve/extension/tools/navigate.ts | 19 +++ .../vercel/eve/extension/tools/observe.ts | 18 +++ .../vercel/eve/extension/tools/search.ts | 27 +++++ .../vercel/eve/extension/tools/session.ts | 29 +++++ examples/integrations/vercel/eve/package.json | 57 +++++++++ .../vercel/eve/pnpm-workspace.yaml | 3 + .../integrations/vercel/eve/tsconfig.json | 13 ++ 29 files changed, 692 insertions(+), 1 deletion(-) create mode 100644 examples/integrations/vercel/eve-example/.env.example create mode 100644 examples/integrations/vercel/eve-example/.gitignore create mode 100644 examples/integrations/vercel/eve-example/.nvmrc create mode 100644 examples/integrations/vercel/eve-example/README.md create mode 100644 examples/integrations/vercel/eve-example/agent/agent.ts create mode 100644 examples/integrations/vercel/eve-example/agent/extensions/browserbase.ts create mode 100644 examples/integrations/vercel/eve-example/agent/instructions.md create mode 100644 examples/integrations/vercel/eve-example/package.json create mode 100644 examples/integrations/vercel/eve-example/tsconfig.json create mode 100644 examples/integrations/vercel/eve/.gitignore create mode 100644 examples/integrations/vercel/eve/.nvmrc create mode 100644 examples/integrations/vercel/eve/README.md create mode 100644 examples/integrations/vercel/eve/extension/extension.ts create mode 100644 examples/integrations/vercel/eve/extension/instructions/browser.md create mode 100644 examples/integrations/vercel/eve/extension/lib/browserbase.ts create mode 100644 examples/integrations/vercel/eve/extension/lib/session-state.ts create mode 100644 examples/integrations/vercel/eve/extension/lib/stagehand.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/act.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/agent.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/extract.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/fetch.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/navigate.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/observe.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/search.ts create mode 100644 examples/integrations/vercel/eve/extension/tools/session.ts create mode 100644 examples/integrations/vercel/eve/package.json create mode 100644 examples/integrations/vercel/eve/pnpm-workspace.yaml create mode 100644 examples/integrations/vercel/eve/tsconfig.json diff --git a/examples/integrations/vercel/README.md b/examples/integrations/vercel/README.md index 40ea742..ca5ce4c 100644 --- a/examples/integrations/vercel/README.md +++ b/examples/integrations/vercel/README.md @@ -14,8 +14,13 @@ Browserbase is a perfect addition to the Vercel AI SDK, bringing headless browse ## Examples -In this directory we have two examples of using Vercel & Browserbase together: +In this directory we have three examples of using Vercel & Browserbase together: BrowseGPT is a chat interface that allows users to search the web and get answers to their questions. It demonstrates how to use Vercel AI SDK with Browserbase to create an AI assistant that can browse the web in real-time to provide up-to-date information. The second example shows how to use Puppeteer with Vercel on Fluid Compute. It demonstrates setting up a Next.js application with Puppeteer for browser automation tasks, deployed on Vercel's Fluid Compute infrastructure which provides longer execution times needed for browser automation. + +The [Eve extension](./eve/) gives Eve agents a persistent Browserbase browser +through Stagehand's `act`, `observe`, `extract`, and autonomous agent primitives. +The sibling [example agent](./eve-example/) mounts the extension in a runnable +pnpm workspace. diff --git a/examples/integrations/vercel/eve-example/.env.example b/examples/integrations/vercel/eve-example/.env.example new file mode 100644 index 0000000..6e7506d --- /dev/null +++ b/examples/integrations/vercel/eve-example/.env.example @@ -0,0 +1,2 @@ +BROWSERBASE_API_KEY=bb_live_... +AI_GATEWAY_API_KEY=your_vercel_ai_gateway_key diff --git a/examples/integrations/vercel/eve-example/.gitignore b/examples/integrations/vercel/eve-example/.gitignore new file mode 100644 index 0000000..3669627 --- /dev/null +++ b/examples/integrations/vercel/eve-example/.gitignore @@ -0,0 +1,9 @@ +node_modules +.env* +!.env.example +.eve +.vercel +.output +.nitro +dist +*.tsbuildinfo diff --git a/examples/integrations/vercel/eve-example/.nvmrc b/examples/integrations/vercel/eve-example/.nvmrc new file mode 100644 index 0000000..b832e40 --- /dev/null +++ b/examples/integrations/vercel/eve-example/.nvmrc @@ -0,0 +1 @@ +24.16.0 diff --git a/examples/integrations/vercel/eve-example/README.md b/examples/integrations/vercel/eve-example/README.md new file mode 100644 index 0000000..db20870 --- /dev/null +++ b/examples/integrations/vercel/eve-example/README.md @@ -0,0 +1,48 @@ +# Example Eve browser agent + +This agent mounts the local `@browserbasehq/eve` extension as `browserbase`, so +its tools are available as `browserbase__create_session`, +`browserbase__navigate`, `browserbase__observe`, `browserbase__act`, +`browserbase__extract`, `browserbase__agent`, and +`browserbase__stop_session`. It also exposes `browserbase__search` and +`browserbase__fetch`, which do not require a browser session. + +## Run it + +From the sibling `eve` extension directory: + +```bash +cd ../eve +nvm use +pnpm install +pnpm build +cp ../eve-example/.env.example ../eve-example/.env +``` + +Fill in the values in `../eve-example/.env`, then start the agent: + +```bash +pnpm --filter browserbase-eve-example dev +``` + +Eve requires Node.js 24 or newer. Both workspace packages include an `.nvmrc` +for Node 24.16.0. If that version is not installed yet, run +`nvm install 24.16.0` before `nvm use`. + +The Eve terminal UI opens. Try this prompt: + +```text +Open https://news.ycombinator.com and return the titles and URLs of the first +five stories. Use a Browserbase session and stop it when you are done. +``` + +The environment requires: + +- `BROWSERBASE_API_KEY` to create the cloud browser and power Stagehand through + Browserbase Model Gateway. +- `AI_GATEWAY_API_KEY` to power the Eve agent model. A linked Vercel project's + `VERCEL_OIDC_TOKEN` can be used instead. + +The dependency on `@browserbasehq/eve` uses pnpm's `workspace:*` protocol, so +the example exercises the sibling extension without publishing it first. The +workspace-level install provides dependencies for both packages. diff --git a/examples/integrations/vercel/eve-example/agent/agent.ts b/examples/integrations/vercel/eve-example/agent/agent.ts new file mode 100644 index 0000000..09892fe --- /dev/null +++ b/examples/integrations/vercel/eve-example/agent/agent.ts @@ -0,0 +1,5 @@ +import { defineAgent } from 'eve'; + +export default defineAgent({ + model: 'openai/gpt-5.4-mini', +}); diff --git a/examples/integrations/vercel/eve-example/agent/extensions/browserbase.ts b/examples/integrations/vercel/eve-example/agent/extensions/browserbase.ts new file mode 100644 index 0000000..5f539b1 --- /dev/null +++ b/examples/integrations/vercel/eve-example/agent/extensions/browserbase.ts @@ -0,0 +1,6 @@ +import browserbase from '@browserbasehq/eve'; + +export default browserbase({ + apiKey: process.env.BROWSERBASE_API_KEY!, + model: 'openai/gpt-5.4-mini', +}); diff --git a/examples/integrations/vercel/eve-example/agent/instructions.md b/examples/integrations/vercel/eve-example/agent/instructions.md new file mode 100644 index 0000000..99aff33 --- /dev/null +++ b/examples/integrations/vercel/eve-example/agent/instructions.md @@ -0,0 +1,19 @@ +# Identity + +You are a browser research agent. Use the Browserbase extension whenever a task +requires opening or interacting with a website. + +# Browser workflow + +1. Use Browserbase Search when you need to discover relevant URLs. +2. Use Browserbase Fetch when you only need page content and the page does not + require JavaScript or interaction. +3. For interactive or rendered pages, create a Browserbase session and navigate + to the requested URL. +4. Use observe before interacting with an unfamiliar page. +5. Prefer act for one interaction and extract for structured results. +6. Use the autonomous agent tool only for genuinely multi-step browser work. +7. Stop any Browserbase session before returning the final answer. + +Never invent page contents. Report a browser failure clearly if a tool cannot +complete the requested operation. diff --git a/examples/integrations/vercel/eve-example/package.json b/examples/integrations/vercel/eve-example/package.json new file mode 100644 index 0000000..1090a6e --- /dev/null +++ b/examples/integrations/vercel/eve-example/package.json @@ -0,0 +1,29 @@ +{ + "name": "browserbase-eve-example", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.9.0", + "scripts": { + "build": "eve build", + "dev": "eve dev", + "start": "eve start", + "typecheck": "tsc" + }, + "dependencies": { + "@browserbasehq/eve": "workspace:*", + "ai": "^7.0.26", + "eve": "0.25.3", + "zod": "4.4.3" + }, + "devDependencies": { + "@types/node": "24.10.1", + "typescript": "7.0.2" + }, + "overrides": { + "ai": "^7.0.26" + }, + "engines": { + "node": ">=24" + } +} diff --git a/examples/integrations/vercel/eve-example/tsconfig.json b/examples/integrations/vercel/eve-example/tsconfig.json new file mode 100644 index 0000000..6a8f493 --- /dev/null +++ b/examples/integrations/vercel/eve-example/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "esnext", + "moduleResolution": "bundler", + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["agent/**/*.ts"] +} diff --git a/examples/integrations/vercel/eve/.gitignore b/examples/integrations/vercel/eve/.gitignore new file mode 100644 index 0000000..95931dd --- /dev/null +++ b/examples/integrations/vercel/eve/.gitignore @@ -0,0 +1,8 @@ +node_modules +.env* +.eve +.vercel +.output +.nitro +dist +*.tsbuildinfo diff --git a/examples/integrations/vercel/eve/.nvmrc b/examples/integrations/vercel/eve/.nvmrc new file mode 100644 index 0000000..b832e40 --- /dev/null +++ b/examples/integrations/vercel/eve/.nvmrc @@ -0,0 +1 @@ +24.16.0 diff --git a/examples/integrations/vercel/eve/README.md b/examples/integrations/vercel/eve/README.md new file mode 100644 index 0000000..fe7f492 --- /dev/null +++ b/examples/integrations/vercel/eve/README.md @@ -0,0 +1,114 @@ +# Browserbase for Eve + +An [Eve extension](https://eve.dev/docs/extensions) that gives an agent +Browserbase Search and Fetch plus a persistent Browserbase browser powered by +[Stagehand](https://docs.stagehand.dev/). It exposes Stagehand's focused +primitives (`act`, `observe`, and `extract`) plus navigation and autonomous agent +tools. + +## Install + +```bash +pnpm add @browserbasehq/eve +``` + +Add your credentials to the consuming Eve app: + +```bash +BROWSERBASE_API_KEY=bb_live_... +``` + +## Mount the extension + +The filename under `agent/extensions/` becomes the tool namespace. Mounting the +extension as `browserbase.ts` creates tools such as `browserbase__search`, +`browserbase__fetch`, `browserbase__create_session`, and +`browserbase__navigate`. + +```ts +// agent/extensions/browserbase.ts +import browserbase from '@browserbasehq/eve'; + +export default browserbase({ + apiKey: process.env.BROWSERBASE_API_KEY!, + model: 'openai/gpt-5.4-mini', +}); +``` + +Start Eve and give the agent a browser task: + +```bash +npx eve dev +``` + +```text +Open https://news.ycombinator.com and extract the titles and URLs of the first +five stories. +``` + +The extension keeps the Browserbase session ID in Eve's durable per-session +state. Each tool reconnects to that browser, performs one operation, and +disconnects without terminating it. `browserbase__create_session` creates or +reconnects the browser explicitly, and `browserbase__stop_session` terminates it. + +## Tools + +| Tool | Purpose | +| ---------------- | --------------------------------------------------------- | +| `search` | Find relevant public web pages with Browserbase Search. | +| `fetch` | Retrieve raw, markdown, or structured page content. | +| `create_session` | Create or reconnect the Browserbase session. | +| `stop_session` | Stop the Browserbase session and release resources. | +| `navigate` | Open a URL in the current browser. | +| `observe` | Find relevant elements and candidate actions. | +| `act` | Perform one natural-language page interaction. | +| `extract` | Return data validated against a supplied JSON Schema. | +| `agent` | Run a multi-step autonomous Stagehand task. | + +Use Search → Fetch → browser as an escalation path: discover sources cheaply, +retrieve straightforward content without a session, and create a browser only +when the page requires JavaScript or interaction. Fetch supports Browserbase's +`raw`, `markdown`, and schema-driven `json` formats. + +For predictable and efficient runs, use `create_session` → `navigate` → +`observe` → `act` or `extract`, then `stop_session`. Reserve `agent` for +workflows that need Stagehand to plan several steps on its own. + +## Configuration + +| Option | Default | Description | +| ----------------------- | --------------------- | ------------------------------------------------------- | +| `apiKey` | required | Browserbase API key for browsers and Model Gateway. | +| `model` | `openai/gpt-5.4-mini` | Stagehand Model Gateway model identifier. | +| `sessionTimeoutSeconds` | `900` | Session timeout, from 60 to 21,600 seconds. | +| `proxies` | `false` | Enable Browserbase proxies for new sessions. | + +Stagehand runs through Browserbase Model Gateway, so consumers do not need an +OpenAI or other model-provider API key. The Browserbase API key covers both the +browser session and Stagehand inference. + +The extension uses Browserbase `keepAlive` sessions so it can reconnect across +Eve workflow steps and Vercel function invocations. Close the session after the +task to avoid leaving billable browser time running. Keep-alive availability +depends on your Browserbase plan. + +## Build + +```bash +nvm use +pnpm install +pnpm check +``` + +Eve requires Node.js 24 or newer. This directory's `.nvmrc` pins Node 24.16.0; +run `nvm install 24.16.0` first if needed. + +`eve extension build` writes the publishable extension and type declarations to +`dist/`. + +## Example agent + +The sibling [`eve-example`](../eve-example/) directory contains a runnable Eve +agent that mounts this package locally. It includes the agent instructions, +environment template, and a Browserbase research prompt for a complete smoke +test. diff --git a/examples/integrations/vercel/eve/extension/extension.ts b/examples/integrations/vercel/eve/extension/extension.ts new file mode 100644 index 0000000..411177d --- /dev/null +++ b/examples/integrations/vercel/eve/extension/extension.ts @@ -0,0 +1,11 @@ +import { defineExtension } from 'eve/extension'; +import { z } from 'zod'; + +export default defineExtension({ + config: z.object({ + apiKey: z.string().min(1), + model: z.string().min(1).default('openai/gpt-5.4-mini'), + sessionTimeoutSeconds: z.number().int().min(60).max(21_600).default(900), + proxies: z.boolean().default(false), + }), +}); diff --git a/examples/integrations/vercel/eve/extension/instructions/browser.md b/examples/integrations/vercel/eve/extension/instructions/browser.md new file mode 100644 index 0000000..6d07b94 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/instructions/browser.md @@ -0,0 +1,7 @@ +Use Browserbase Search to discover relevant URLs and Browserbase Fetch for quick +retrievals that do not need JavaScript or interaction. Escalate to a browser +session when a page needs rendering or interaction: create a session, navigate, +then use observe to plan, act for one interaction, and extract for structured +results. Use agent only for genuinely multi-step work. Browser tools share one +Browserbase session per Eve session, so call them sequentially. Stop the browser +session when the task is complete. diff --git a/examples/integrations/vercel/eve/extension/lib/browserbase.ts b/examples/integrations/vercel/eve/extension/lib/browserbase.ts new file mode 100644 index 0000000..0c5886f --- /dev/null +++ b/examples/integrations/vercel/eve/extension/lib/browserbase.ts @@ -0,0 +1,7 @@ +import Browserbase from '@browserbasehq/sdk'; + +import extension from '../extension'; + +export function createBrowserbaseClient(): Browserbase { + return new Browserbase({ apiKey: extension.config.apiKey }); +} diff --git a/examples/integrations/vercel/eve/extension/lib/session-state.ts b/examples/integrations/vercel/eve/extension/lib/session-state.ts new file mode 100644 index 0000000..c541e31 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/lib/session-state.ts @@ -0,0 +1,11 @@ +import { defineState } from 'eve/context'; + +export interface BrowserSessionState { + id: string | null; + url: string | null; +} + +export const browserSession = defineState( + 'browser-session', + () => ({ id: null, url: null }) +); diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/examples/integrations/vercel/eve/extension/lib/stagehand.ts new file mode 100644 index 0000000..41c73f0 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/lib/stagehand.ts @@ -0,0 +1,103 @@ +import { Stagehand } from '@browserbasehq/stagehand'; + +import extension from '../extension'; +import { browserSession, type BrowserSessionState } from './session-state'; + +type StagehandOperation = (stagehand: Stagehand) => Promise; + +function createStagehand(): Stagehand { + const { apiKey, model, proxies, sessionTimeoutSeconds } = extension.config; + + return new Stagehand({ + env: 'BROWSERBASE', + apiKey, + model, + disablePino: true, + keepAlive: true, + browserbaseSessionCreateParams: { + keepAlive: true, + timeout: sessionTimeoutSeconds, + proxies, + }, + }); +} + +function resumeStagehand(sessionId: string): Stagehand { + const { apiKey, model } = extension.config; + + return new Stagehand({ + env: 'BROWSERBASE', + apiKey, + model, + disablePino: true, + keepAlive: true, + browserbaseSessionID: sessionId, + }); +} + +async function connect(): Promise { + const current = browserSession.get(); + + if (current.id) { + const resumed = resumeStagehand(current.id); + try { + await resumed.init(); + return resumed; + } catch { + browserSession.update(() => ({ id: null, url: null })); + } + } + + const created = createStagehand(); + await created.init(); + browserSession.update(() => ({ + id: created.browserbaseSessionID ?? null, + url: created.browserbaseSessionURL ?? null, + })); + return created; +} + +export async function withStagehand( + operation: StagehandOperation +): Promise { + const stagehand = await connect(); + try { + return await operation(stagehand); + } finally { + await stagehand.close(); + } +} + +export async function createBrowserSession(): Promise { + const stagehand = await connect(); + try { + return browserSession.get(); + } finally { + await stagehand.close(); + } +} + +export async function closeBrowserSession(): Promise { + const current = browserSession.get(); + if (!current.id) return current; + + const stagehand = new Stagehand({ + env: 'BROWSERBASE', + apiKey: extension.config.apiKey, + browserbaseSessionID: current.id, + disablePino: true, + // Eve bundles Stagehand without its CLI-only crash supervisor entrypoint. + // This path closes the session explicitly, so supervisor logging is not useful. + logger: () => {}, + keepAlive: false, + }); + + try { + await stagehand.init(); + await stagehand.close(); + } finally { + browserSession.update(() => ({ id: null, url: null })); + } + + return current; +} diff --git a/examples/integrations/vercel/eve/extension/tools/act.ts b/examples/integrations/vercel/eve/extension/tools/act.ts new file mode 100644 index 0000000..1adee92 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/act.ts @@ -0,0 +1,20 @@ +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { withStagehand } from '../lib/stagehand'; + +export default defineTool({ + description: + 'Perform one natural-language action in the current Browserbase page with Stagehand.', + inputSchema: z.object({ + instruction: z + .string() + .min(1) + .describe( + 'A precise single action, such as clicking or filling a field.' + ), + }), + async execute({ instruction }) { + return withStagehand(stagehand => stagehand.act(instruction)); + }, +}); diff --git a/examples/integrations/vercel/eve/extension/tools/agent.ts b/examples/integrations/vercel/eve/extension/tools/agent.ts new file mode 100644 index 0000000..01af6a4 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/agent.ts @@ -0,0 +1,22 @@ +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { withStagehand } from '../lib/stagehand'; + +export default defineTool({ + description: + 'Run an autonomous multi-step Stagehand browser task in the current Browserbase session. Prefer act, observe, and extract for targeted work.', + inputSchema: z.object({ + instruction: z.string().min(1).describe('The browser task to complete.'), + maxSteps: z.number().int().min(1).max(50).default(20), + }), + async execute({ instruction, maxSteps }) { + return withStagehand(async stagehand => { + const agent = stagehand.agent(); + return agent.execute({ + instruction, + maxSteps, + }); + }); + }, +}); diff --git a/examples/integrations/vercel/eve/extension/tools/extract.ts b/examples/integrations/vercel/eve/extension/tools/extract.ts new file mode 100644 index 0000000..35cc7d8 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/extract.ts @@ -0,0 +1,23 @@ +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { withStagehand } from '../lib/stagehand'; + +const jsonSchema = z + .record(z.string(), z.unknown()) + .describe('A JSON Schema describing the exact data shape to return.'); + +export default defineTool({ + description: + 'Extract structured data from the current Browserbase page with Stagehand and validate it against a JSON Schema.', + inputSchema: z.object({ + instruction: z.string().min(1).describe('What data to extract.'), + schema: jsonSchema, + }), + async execute({ instruction, schema }) { + const outputSchema = z.fromJSONSchema(schema); + return withStagehand(stagehand => + stagehand.extract(instruction, outputSchema) + ); + }, +}); diff --git a/examples/integrations/vercel/eve/extension/tools/fetch.ts b/examples/integrations/vercel/eve/extension/tools/fetch.ts new file mode 100644 index 0000000..1365edd --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/fetch.ts @@ -0,0 +1,61 @@ +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { createBrowserbaseClient } from '../lib/browserbase'; + +const jsonSchema = z + .record(z.string(), z.unknown()) + .optional() + .describe( + 'A JSON Schema for structured extraction. Required with the json format and invalid with other formats.' + ); + +export default defineTool({ + description: + 'Retrieve a URL through Browserbase Fetch without starting a browser session. Use raw for the original response, markdown for agent-friendly content, or json with a schema for structured extraction.', + inputSchema: z.object({ + url: z.url().describe('The absolute URL to retrieve.'), + format: z + .enum(['raw', 'markdown', 'json']) + .default('raw') + .describe('The response content format.'), + schema: jsonSchema, + allowRedirects: z + .boolean() + .default(false) + .describe('Whether to follow HTTP redirects.'), + allowInsecureSsl: z + .boolean() + .default(false) + .describe('Whether to bypass TLS certificate verification.'), + proxies: z + .boolean() + .default(false) + .describe('Whether to route the request through Browserbase proxies.'), + }), + async execute({ + url, + format, + schema, + allowRedirects, + allowInsecureSsl, + proxies, + }) { + if (format === 'json' && !schema) { + throw new Error('schema is required when format is json.'); + } + if (format !== 'json' && schema) { + throw new Error('schema can only be used when format is json.'); + } + + const browserbase = createBrowserbaseClient(); + return browserbase.fetchAPI.create({ + url, + format, + schema, + allowRedirects, + allowInsecureSsl, + proxies, + }); + }, +}); diff --git a/examples/integrations/vercel/eve/extension/tools/navigate.ts b/examples/integrations/vercel/eve/extension/tools/navigate.ts new file mode 100644 index 0000000..2029949 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/navigate.ts @@ -0,0 +1,19 @@ +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { withStagehand } from '../lib/stagehand'; + +export default defineTool({ + description: + 'Navigate the Browserbase browser to a URL. Call this before using other browser tools on a new site.', + inputSchema: z.object({ + url: z.url().describe('The absolute URL to open.'), + }), + async execute({ url }) { + return withStagehand(async stagehand => { + const page = stagehand.context.pages()[0]; + await page.goto(url); + return { url: page.url(), title: await page.title() }; + }); + }, +}); diff --git a/examples/integrations/vercel/eve/extension/tools/observe.ts b/examples/integrations/vercel/eve/extension/tools/observe.ts new file mode 100644 index 0000000..d933750 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/observe.ts @@ -0,0 +1,18 @@ +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { withStagehand } from '../lib/stagehand'; + +export default defineTool({ + description: + 'Inspect the current Browserbase page with Stagehand and return candidate elements and actions.', + inputSchema: z.object({ + instruction: z + .string() + .min(1) + .describe('The elements or actions to find on the page.'), + }), + async execute({ instruction }) { + return withStagehand(stagehand => stagehand.observe(instruction)); + }, +}); diff --git a/examples/integrations/vercel/eve/extension/tools/search.ts b/examples/integrations/vercel/eve/extension/tools/search.ts new file mode 100644 index 0000000..6f7fa7b --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/search.ts @@ -0,0 +1,27 @@ +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { createBrowserbaseClient } from '../lib/browserbase'; + +export default defineTool({ + description: + 'Search the public web with Browserbase Search. Use this to discover relevant URLs before fetching content or opening a browser session.', + inputSchema: z.object({ + query: z + .string() + .min(1) + .max(200) + .describe('The web search query, from 1 to 200 characters.'), + numResults: z + .number() + .int() + .min(1) + .max(25) + .default(10) + .describe('The number of results to return, from 1 to 25.'), + }), + async execute({ query, numResults }) { + const browserbase = createBrowserbaseClient(); + return browserbase.search.web({ query, numResults }); + }, +}); diff --git a/examples/integrations/vercel/eve/extension/tools/session.ts b/examples/integrations/vercel/eve/extension/tools/session.ts new file mode 100644 index 0000000..e455ec4 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/tools/session.ts @@ -0,0 +1,29 @@ +import { defineDynamic, defineTool } from 'eve/tools'; +import { z } from 'zod'; + +import { closeBrowserSession, createBrowserSession } from '../lib/stagehand'; + +export default defineDynamic({ + events: { + 'session.started': () => ({ + create_session: defineTool({ + description: + 'Create a Browserbase browser session for this Eve conversation, or reconnect if one already exists.', + inputSchema: z.object({}), + async execute() { + const session = await createBrowserSession(); + return { ...session, created: Boolean(session.id) }; + }, + }), + stop_session: defineTool({ + description: + 'Stop the Browserbase browser session for this Eve conversation and release its resources.', + inputSchema: z.object({}), + async execute() { + const session = await closeBrowserSession(); + return { ...session, stopped: Boolean(session.id) }; + }, + }), + }), + }, +}); diff --git a/examples/integrations/vercel/eve/package.json b/examples/integrations/vercel/eve/package.json new file mode 100644 index 0000000..bba8e8d --- /dev/null +++ b/examples/integrations/vercel/eve/package.json @@ -0,0 +1,57 @@ +{ + "name": "@browserbasehq/eve", + "version": "0.1.0", + "description": "An Eve extension that gives agents Browserbase-powered Stagehand browser tools.", + "type": "module", + "packageManager": "pnpm@10.9.0", + "license": "MIT", + "keywords": [ + "browserbase", + "eve", + "stagehand", + "browser-automation", + "ai-agents" + ], + "eve": { + "extension": { + "source": "./extension", + "dist": "./dist/extension" + } + }, + "files": [ + "dist", + "README.md" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./tools": { + "types": "./dist/tools/index.d.ts", + "default": "./dist/tools/index.mjs" + } + }, + "scripts": { + "build": "eve extension build", + "prepare": "eve extension build", + "typecheck": "tsc", + "check": "pnpm typecheck && pnpm build" + }, + "dependencies": { + "@browserbasehq/sdk": "2.16.0", + "@browserbasehq/stagehand": "3.7.0", + "zod": "4.4.3" + }, + "devDependencies": { + "@types/node": "24.10.1", + "eve": "0.25.3", + "typescript": "7.0.2" + }, + "peerDependencies": { + "eve": "*" + }, + "engines": { + "node": ">=24" + } +} diff --git a/examples/integrations/vercel/eve/pnpm-workspace.yaml b/examples/integrations/vercel/eve/pnpm-workspace.yaml new file mode 100644 index 0000000..0c0544b --- /dev/null +++ b/examples/integrations/vercel/eve/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - '.' + - '../eve-example' diff --git a/examples/integrations/vercel/eve/tsconfig.json b/examples/integrations/vercel/eve/tsconfig.json new file mode 100644 index 0000000..ba1682b --- /dev/null +++ b/examples/integrations/vercel/eve/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "esnext", + "moduleResolution": "bundler", + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["extension/**/*.ts"] +} From 8acdd80f86c38d1ce71c8e9fe562c531293df68d Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Tue, 21 Jul 2026 14:43:48 -0700 Subject: [PATCH 2/8] Fix Eve session lifecycle handling --- .../vercel/eve/extension/lib/stagehand.ts | 52 ++++++++++++++----- .../vercel/eve/extension/tools/navigate.ts | 2 +- .../vercel/eve/extension/tools/session.ts | 3 +- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/examples/integrations/vercel/eve/extension/lib/stagehand.ts index 41c73f0..e5a4932 100644 --- a/examples/integrations/vercel/eve/extension/lib/stagehand.ts +++ b/examples/integrations/vercel/eve/extension/lib/stagehand.ts @@ -1,10 +1,31 @@ import { Stagehand } from '@browserbasehq/stagehand'; import extension from '../extension'; +import { createBrowserbaseClient } from './browserbase'; import { browserSession, type BrowserSessionState } from './session-state'; type StagehandOperation = (stagehand: Stagehand) => Promise; +interface StagehandConnection { + stagehand: Stagehand; + created: boolean; +} + +export interface BrowserSessionResult extends BrowserSessionState { + created: boolean; +} + +async function isTerminalBrowserSession(sessionId: string): Promise { + try { + const session = await createBrowserbaseClient().sessions.retrieve(sessionId); + return !['PENDING', 'RUNNING'].includes(session.status); + } catch { + // A failed status check may be transient. Preserve the durable session id so + // the next tool call can retry instead of leaking a keep-alive session. + return false; + } +} + function createStagehand(): Stagehand { const { apiKey, model, proxies, sessionTimeoutSeconds } = extension.config; @@ -35,15 +56,21 @@ function resumeStagehand(sessionId: string): Stagehand { }); } -async function connect(): Promise { +async function connect(): Promise { const current = browserSession.get(); if (current.id) { const resumed = resumeStagehand(current.id); try { await resumed.init(); - return resumed; - } catch { + return { stagehand: resumed, created: false }; + } catch (error) { + await resumed.close().catch(() => {}); + + if (!(await isTerminalBrowserSession(current.id))) { + throw error; + } + browserSession.update(() => ({ id: null, url: null })); } } @@ -54,13 +81,13 @@ async function connect(): Promise { id: created.browserbaseSessionID ?? null, url: created.browserbaseSessionURL ?? null, })); - return created; + return { stagehand: created, created: true }; } export async function withStagehand( operation: StagehandOperation ): Promise { - const stagehand = await connect(); + const { stagehand } = await connect(); try { return await operation(stagehand); } finally { @@ -68,10 +95,10 @@ export async function withStagehand( } } -export async function createBrowserSession(): Promise { - const stagehand = await connect(); +export async function createBrowserSession(): Promise { + const { stagehand, created } = await connect(); try { - return browserSession.get(); + return { ...browserSession.get(), created }; } finally { await stagehand.close(); } @@ -92,12 +119,9 @@ export async function closeBrowserSession(): Promise { keepAlive: false, }); - try { - await stagehand.init(); - await stagehand.close(); - } finally { - browserSession.update(() => ({ id: null, url: null })); - } + await stagehand.init(); + await stagehand.close(); + browserSession.update(() => ({ id: null, url: null })); return current; } diff --git a/examples/integrations/vercel/eve/extension/tools/navigate.ts b/examples/integrations/vercel/eve/extension/tools/navigate.ts index 2029949..8a97584 100644 --- a/examples/integrations/vercel/eve/extension/tools/navigate.ts +++ b/examples/integrations/vercel/eve/extension/tools/navigate.ts @@ -11,7 +11,7 @@ export default defineTool({ }), async execute({ url }) { return withStagehand(async stagehand => { - const page = stagehand.context.pages()[0]; + const page = await stagehand.context.awaitActivePage(); await page.goto(url); return { url: page.url(), title: await page.title() }; }); diff --git a/examples/integrations/vercel/eve/extension/tools/session.ts b/examples/integrations/vercel/eve/extension/tools/session.ts index e455ec4..c209f46 100644 --- a/examples/integrations/vercel/eve/extension/tools/session.ts +++ b/examples/integrations/vercel/eve/extension/tools/session.ts @@ -11,8 +11,7 @@ export default defineDynamic({ 'Create a Browserbase browser session for this Eve conversation, or reconnect if one already exists.', inputSchema: z.object({}), async execute() { - const session = await createBrowserSession(); - return { ...session, created: Boolean(session.id) }; + return createBrowserSession(); }, }), stop_session: defineTool({ From 263a6dbcd11eab3643f8fbdf693c3c7e803a49a1 Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Tue, 21 Jul 2026 15:03:14 -0700 Subject: [PATCH 3/8] Serialize Eve browser session access --- .../eve/extension/instructions/browser.md | 5 +- .../vercel/eve/extension/lib/session-lock.ts | 24 ++++ .../vercel/eve/extension/lib/stagehand.ts | 104 +++++++++++------- .../vercel/eve/extension/tools/act.ts | 6 +- .../vercel/eve/extension/tools/agent.ts | 4 +- .../vercel/eve/extension/tools/extract.ts | 4 +- .../vercel/eve/extension/tools/navigate.ts | 4 +- .../vercel/eve/extension/tools/observe.ts | 6 +- .../vercel/eve/extension/tools/session.ts | 8 +- examples/integrations/vercel/eve/package.json | 3 +- .../vercel/eve/tests/session-lock.test.ts | 69 ++++++++++++ .../integrations/vercel/eve/tsconfig.json | 3 +- 12 files changed, 185 insertions(+), 55 deletions(-) create mode 100644 examples/integrations/vercel/eve/extension/lib/session-lock.ts create mode 100644 examples/integrations/vercel/eve/tests/session-lock.test.ts diff --git a/examples/integrations/vercel/eve/extension/instructions/browser.md b/examples/integrations/vercel/eve/extension/instructions/browser.md index 6d07b94..fb68e6c 100644 --- a/examples/integrations/vercel/eve/extension/instructions/browser.md +++ b/examples/integrations/vercel/eve/extension/instructions/browser.md @@ -3,5 +3,6 @@ retrievals that do not need JavaScript or interaction. Escalate to a browser session when a page needs rendering or interaction: create a session, navigate, then use observe to plan, act for one interaction, and extract for structured results. Use agent only for genuinely multi-step work. Browser tools share one -Browserbase session per Eve session, so call them sequentially. Stop the browser -session when the task is complete. +Browserbase session per Eve session and the extension serializes their access. +Prefer calling them sequentially, and stop the browser session when the task is +complete. diff --git a/examples/integrations/vercel/eve/extension/lib/session-lock.ts b/examples/integrations/vercel/eve/extension/lib/session-lock.ts new file mode 100644 index 0000000..3fd615e --- /dev/null +++ b/examples/integrations/vercel/eve/extension/lib/session-lock.ts @@ -0,0 +1,24 @@ +const sessionTails = new Map>(); + +export async function withSessionLock( + sessionId: string, + operation: () => Promise +): Promise { + const previous = sessionTails.get(sessionId) ?? Promise.resolve(); + let release = () => {}; + const current = new Promise(resolve => { + release = resolve; + }); + + sessionTails.set(sessionId, current); + await previous; + + try { + return await operation(); + } finally { + release(); + if (sessionTails.get(sessionId) === current) { + sessionTails.delete(sessionId); + } + } +} diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/examples/integrations/vercel/eve/extension/lib/stagehand.ts index e5a4932..2de10cf 100644 --- a/examples/integrations/vercel/eve/extension/lib/stagehand.ts +++ b/examples/integrations/vercel/eve/extension/lib/stagehand.ts @@ -1,7 +1,9 @@ import { Stagehand } from '@browserbasehq/stagehand'; +import { APIError } from '@browserbasehq/sdk'; import extension from '../extension'; import { createBrowserbaseClient } from './browserbase'; +import { withSessionLock } from './session-lock'; import { browserSession, type BrowserSessionState } from './session-state'; type StagehandOperation = (stagehand: Stagehand) => Promise; @@ -15,14 +17,20 @@ export interface BrowserSessionResult extends BrowserSessionState { created: boolean; } -async function isTerminalBrowserSession(sessionId: string): Promise { +type RemoteSessionState = 'active' | 'terminal' | 'unknown'; + +async function getRemoteSessionState( + sessionId: string +): Promise { try { const session = await createBrowserbaseClient().sessions.retrieve(sessionId); - return !['PENDING', 'RUNNING'].includes(session.status); - } catch { - // A failed status check may be transient. Preserve the durable session id so - // the next tool call can retry instead of leaking a keep-alive session. - return false; + return ['PENDING', 'RUNNING'].includes(session.status) + ? 'active' + : 'terminal'; + } catch (error) { + return error instanceof APIError && error.status === 404 + ? 'terminal' + : 'unknown'; } } @@ -67,7 +75,7 @@ async function connect(): Promise { } catch (error) { await resumed.close().catch(() => {}); - if (!(await isTerminalBrowserSession(current.id))) { + if ((await getRemoteSessionState(current.id)) !== 'terminal') { throw error; } @@ -85,43 +93,65 @@ async function connect(): Promise { } export async function withStagehand( + sessionId: string, operation: StagehandOperation ): Promise { - const { stagehand } = await connect(); - try { - return await operation(stagehand); - } finally { - await stagehand.close(); - } + return withSessionLock(sessionId, async () => { + const { stagehand } = await connect(); + try { + return await operation(stagehand); + } finally { + await stagehand.close(); + } + }); } -export async function createBrowserSession(): Promise { - const { stagehand, created } = await connect(); - try { - return { ...browserSession.get(), created }; - } finally { - await stagehand.close(); - } +export async function createBrowserSession( + sessionId: string +): Promise { + return withSessionLock(sessionId, async () => { + const { stagehand, created } = await connect(); + try { + return { ...browserSession.get(), created }; + } finally { + await stagehand.close(); + } + }); } -export async function closeBrowserSession(): Promise { - const current = browserSession.get(); - if (!current.id) return current; +export async function closeBrowserSession( + sessionId: string +): Promise { + return withSessionLock(sessionId, async () => { + const current = browserSession.get(); + if (!current.id) return current; - const stagehand = new Stagehand({ - env: 'BROWSERBASE', - apiKey: extension.config.apiKey, - browserbaseSessionID: current.id, - disablePino: true, - // Eve bundles Stagehand without its CLI-only crash supervisor entrypoint. - // This path closes the session explicitly, so supervisor logging is not useful. - logger: () => {}, - keepAlive: false, - }); + if ((await getRemoteSessionState(current.id)) === 'terminal') { + browserSession.update(() => ({ id: null, url: null })); + return current; + } + + const stagehand = new Stagehand({ + env: 'BROWSERBASE', + apiKey: extension.config.apiKey, + browserbaseSessionID: current.id, + disablePino: true, + // Eve bundles Stagehand without its CLI-only crash supervisor entrypoint. + // This path closes the session explicitly, so supervisor logging is not useful. + logger: () => {}, + keepAlive: false, + }); - await stagehand.init(); - await stagehand.close(); - browserSession.update(() => ({ id: null, url: null })); + try { + await stagehand.init(); + await stagehand.close(); + } catch (error) { + if ((await getRemoteSessionState(current.id)) !== 'terminal') { + throw error; + } + } - return current; + browserSession.update(() => ({ id: null, url: null })); + return current; + }); } diff --git a/examples/integrations/vercel/eve/extension/tools/act.ts b/examples/integrations/vercel/eve/extension/tools/act.ts index 1adee92..d902fe1 100644 --- a/examples/integrations/vercel/eve/extension/tools/act.ts +++ b/examples/integrations/vercel/eve/extension/tools/act.ts @@ -14,7 +14,9 @@ export default defineTool({ 'A precise single action, such as clicking or filling a field.' ), }), - async execute({ instruction }) { - return withStagehand(stagehand => stagehand.act(instruction)); + async execute({ instruction }, ctx) { + return withStagehand(ctx.session.id, stagehand => + stagehand.act(instruction) + ); }, }); diff --git a/examples/integrations/vercel/eve/extension/tools/agent.ts b/examples/integrations/vercel/eve/extension/tools/agent.ts index 01af6a4..2d5ceb6 100644 --- a/examples/integrations/vercel/eve/extension/tools/agent.ts +++ b/examples/integrations/vercel/eve/extension/tools/agent.ts @@ -10,8 +10,8 @@ export default defineTool({ instruction: z.string().min(1).describe('The browser task to complete.'), maxSteps: z.number().int().min(1).max(50).default(20), }), - async execute({ instruction, maxSteps }) { - return withStagehand(async stagehand => { + async execute({ instruction, maxSteps }, ctx) { + return withStagehand(ctx.session.id, async stagehand => { const agent = stagehand.agent(); return agent.execute({ instruction, diff --git a/examples/integrations/vercel/eve/extension/tools/extract.ts b/examples/integrations/vercel/eve/extension/tools/extract.ts index 35cc7d8..a2b722f 100644 --- a/examples/integrations/vercel/eve/extension/tools/extract.ts +++ b/examples/integrations/vercel/eve/extension/tools/extract.ts @@ -14,9 +14,9 @@ export default defineTool({ instruction: z.string().min(1).describe('What data to extract.'), schema: jsonSchema, }), - async execute({ instruction, schema }) { + async execute({ instruction, schema }, ctx) { const outputSchema = z.fromJSONSchema(schema); - return withStagehand(stagehand => + return withStagehand(ctx.session.id, stagehand => stagehand.extract(instruction, outputSchema) ); }, diff --git a/examples/integrations/vercel/eve/extension/tools/navigate.ts b/examples/integrations/vercel/eve/extension/tools/navigate.ts index 8a97584..3f778d5 100644 --- a/examples/integrations/vercel/eve/extension/tools/navigate.ts +++ b/examples/integrations/vercel/eve/extension/tools/navigate.ts @@ -9,8 +9,8 @@ export default defineTool({ inputSchema: z.object({ url: z.url().describe('The absolute URL to open.'), }), - async execute({ url }) { - return withStagehand(async stagehand => { + async execute({ url }, ctx) { + return withStagehand(ctx.session.id, async stagehand => { const page = await stagehand.context.awaitActivePage(); await page.goto(url); return { url: page.url(), title: await page.title() }; diff --git a/examples/integrations/vercel/eve/extension/tools/observe.ts b/examples/integrations/vercel/eve/extension/tools/observe.ts index d933750..8365668 100644 --- a/examples/integrations/vercel/eve/extension/tools/observe.ts +++ b/examples/integrations/vercel/eve/extension/tools/observe.ts @@ -12,7 +12,9 @@ export default defineTool({ .min(1) .describe('The elements or actions to find on the page.'), }), - async execute({ instruction }) { - return withStagehand(stagehand => stagehand.observe(instruction)); + async execute({ instruction }, ctx) { + return withStagehand(ctx.session.id, stagehand => + stagehand.observe(instruction) + ); }, }); diff --git a/examples/integrations/vercel/eve/extension/tools/session.ts b/examples/integrations/vercel/eve/extension/tools/session.ts index c209f46..f57d3d0 100644 --- a/examples/integrations/vercel/eve/extension/tools/session.ts +++ b/examples/integrations/vercel/eve/extension/tools/session.ts @@ -10,16 +10,16 @@ export default defineDynamic({ description: 'Create a Browserbase browser session for this Eve conversation, or reconnect if one already exists.', inputSchema: z.object({}), - async execute() { - return createBrowserSession(); + async execute(_input, ctx) { + return createBrowserSession(ctx.session.id); }, }), stop_session: defineTool({ description: 'Stop the Browserbase browser session for this Eve conversation and release its resources.', inputSchema: z.object({}), - async execute() { - const session = await closeBrowserSession(); + async execute(_input, ctx) { + const session = await closeBrowserSession(ctx.session.id); return { ...session, stopped: Boolean(session.id) }; }, }), diff --git a/examples/integrations/vercel/eve/package.json b/examples/integrations/vercel/eve/package.json index bba8e8d..38bdf35 100644 --- a/examples/integrations/vercel/eve/package.json +++ b/examples/integrations/vercel/eve/package.json @@ -36,7 +36,8 @@ "build": "eve extension build", "prepare": "eve extension build", "typecheck": "tsc", - "check": "pnpm typecheck && pnpm build" + "check": "pnpm typecheck && pnpm test && pnpm build", + "test": "node --test tests/*.test.ts" }, "dependencies": { "@browserbasehq/sdk": "2.16.0", diff --git a/examples/integrations/vercel/eve/tests/session-lock.test.ts b/examples/integrations/vercel/eve/tests/session-lock.test.ts new file mode 100644 index 0000000..450adcc --- /dev/null +++ b/examples/integrations/vercel/eve/tests/session-lock.test.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { withSessionLock } from '../extension/lib/session-lock.ts'; + +test('serializes operations within one Eve session', async () => { + const events: string[] = []; + let releaseFirst = () => {}; + const firstBlocked = new Promise(resolve => { + releaseFirst = resolve; + }); + + const first = withSessionLock('session-a', async () => { + events.push('first:start'); + await firstBlocked; + events.push('first:end'); + }); + const second = withSessionLock('session-a', async () => { + events.push('second:start'); + events.push('second:end'); + }); + + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(events, ['first:start']); + + releaseFirst(); + await Promise.all([first, second]); + assert.deepEqual(events, [ + 'first:start', + 'first:end', + 'second:start', + 'second:end', + ]); +}); + +test('does not serialize operations from different Eve sessions', async () => { + const started: string[] = []; + let release = () => {}; + const blocked = new Promise(resolve => { + release = resolve; + }); + + const first = withSessionLock('session-a', async () => { + started.push('session-a'); + await blocked; + }); + const second = withSessionLock('session-b', async () => { + started.push('session-b'); + await blocked; + }); + + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(started.sort(), ['session-a', 'session-b']); + + release(); + await Promise.all([first, second]); +}); + +test('releases a session lock when an operation fails', async () => { + await assert.rejects( + withSessionLock('session-a', async () => { + throw new Error('expected failure'); + }), + /expected failure/ + ); + + const result = await withSessionLock('session-a', async () => 'recovered'); + assert.equal(result, 'recovered'); +}); diff --git a/examples/integrations/vercel/eve/tsconfig.json b/examples/integrations/vercel/eve/tsconfig.json index ba1682b..5174a94 100644 --- a/examples/integrations/vercel/eve/tsconfig.json +++ b/examples/integrations/vercel/eve/tsconfig.json @@ -3,11 +3,12 @@ "target": "ES2022", "module": "esnext", "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "types": ["node"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "noEmit": true }, - "include": ["extension/**/*.ts"] + "include": ["extension/**/*.ts", "tests/**/*.ts"] } From 98a08394428a6ded24fecc90a7dd430a1a3e7530 Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Tue, 21 Jul 2026 15:54:21 -0700 Subject: [PATCH 4/8] Harden Eve Stagehand session cleanup --- examples/integrations/vercel/eve/README.md | 8 +-- .../eve/extension/instructions/browser.md | 7 +-- .../vercel/eve/extension/lib/disconnect.ts | 9 ++++ .../vercel/eve/extension/lib/session-lock.ts | 3 ++ .../vercel/eve/extension/lib/stagehand.ts | 54 ++++++++++++++++--- .../vercel/eve/tests/stagehand.test.ts | 14 +++++ 6 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 examples/integrations/vercel/eve/extension/lib/disconnect.ts create mode 100644 examples/integrations/vercel/eve/tests/stagehand.test.ts diff --git a/examples/integrations/vercel/eve/README.md b/examples/integrations/vercel/eve/README.md index fe7f492..593d9b9 100644 --- a/examples/integrations/vercel/eve/README.md +++ b/examples/integrations/vercel/eve/README.md @@ -88,9 +88,11 @@ OpenAI or other model-provider API key. The Browserbase API key covers both the browser session and Stagehand inference. The extension uses Browserbase `keepAlive` sessions so it can reconnect across -Eve workflow steps and Vercel function invocations. Close the session after the -task to avoid leaving billable browser time running. Keep-alive availability -depends on your Browserbase plan. +Eve workflow steps and Vercel function invocations. Parallel browser calls made +inside one Eve workflow step are queued in that step's managed runtime; durable +state reconnects later steps and invocations. Close the session after the task +to avoid leaving billable browser time running. Keep-alive availability depends +on your Browserbase plan. ## Build diff --git a/examples/integrations/vercel/eve/extension/instructions/browser.md b/examples/integrations/vercel/eve/extension/instructions/browser.md index fb68e6c..5e18a10 100644 --- a/examples/integrations/vercel/eve/extension/instructions/browser.md +++ b/examples/integrations/vercel/eve/extension/instructions/browser.md @@ -3,6 +3,7 @@ retrievals that do not need JavaScript or interaction. Escalate to a browser session when a page needs rendering or interaction: create a session, navigate, then use observe to plan, act for one interaction, and extract for structured results. Use agent only for genuinely multi-step work. Browser tools share one -Browserbase session per Eve session and the extension serializes their access. -Prefer calling them sequentially, and stop the browser session when the task is -complete. +Browserbase session per Eve session. Parallel browser calls from the same Eve +workflow step are queued within that step; later steps reconnect through durable +state. Prefer calling tools sequentially, and stop the browser session when the +task is complete. diff --git a/examples/integrations/vercel/eve/extension/lib/disconnect.ts b/examples/integrations/vercel/eve/extension/lib/disconnect.ts new file mode 100644 index 0000000..52d23f2 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/lib/disconnect.ts @@ -0,0 +1,9 @@ +interface ClosableStagehand { + close(): Promise; +} + +export async function disconnectStagehand( + stagehand: ClosableStagehand +): Promise { + await stagehand.close().catch(() => {}); +} diff --git a/examples/integrations/vercel/eve/extension/lib/session-lock.ts b/examples/integrations/vercel/eve/extension/lib/session-lock.ts index 3fd615e..da297b2 100644 --- a/examples/integrations/vercel/eve/extension/lib/session-lock.ts +++ b/examples/integrations/vercel/eve/extension/lib/session-lock.ts @@ -1,3 +1,6 @@ +// Eve executes parallel tool calls from one model step in the same managed +// runtime. Durable state carries the session ID between later workflow steps; +// this lock only queues the parallel calls within the current step/runtime. const sessionTails = new Map>(); export async function withSessionLock( diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/examples/integrations/vercel/eve/extension/lib/stagehand.ts index 2de10cf..ca858e7 100644 --- a/examples/integrations/vercel/eve/extension/lib/stagehand.ts +++ b/examples/integrations/vercel/eve/extension/lib/stagehand.ts @@ -1,8 +1,11 @@ +import { randomUUID } from 'node:crypto'; + import { Stagehand } from '@browserbasehq/stagehand'; import { APIError } from '@browserbasehq/sdk'; import extension from '../extension'; import { createBrowserbaseClient } from './browserbase'; +import { disconnectStagehand } from './disconnect'; import { withSessionLock } from './session-lock'; import { browserSession, type BrowserSessionState } from './session-state'; @@ -18,13 +21,18 @@ export interface BrowserSessionResult extends BrowserSessionState { } type RemoteSessionState = 'active' | 'terminal' | 'unknown'; +const ACTIVE_SESSION_STATUSES = new Set(['PENDING', 'RUNNING']); + +function browserbaseSessionUrl(sessionId: string): string { + return `https://www.browserbase.com/sessions/${sessionId}`; +} async function getRemoteSessionState( sessionId: string ): Promise { try { const session = await createBrowserbaseClient().sessions.retrieve(sessionId); - return ['PENDING', 'RUNNING'].includes(session.status) + return ACTIVE_SESSION_STATUSES.has(session.status) ? 'active' : 'terminal'; } catch (error) { @@ -34,7 +42,29 @@ async function getRemoteSessionState( } } -function createStagehand(): Stagehand { +async function persistPartiallyCreatedSession( + initAttemptId: string +): Promise { + try { + const sessions = await createBrowserbaseClient().sessions.list({ + q: `user_metadata['eve']['initAttemptId']:'${initAttemptId}'`, + }); + const active = sessions.find(session => + ACTIVE_SESSION_STATUSES.has(session.status) + ); + if (!active) return; + + browserSession.update(() => ({ + id: active.id, + url: browserbaseSessionUrl(active.id), + })); + } catch { + // Preserve the original Stagehand init error. The attempt metadata remains + // attached in Browserbase for diagnosis if recovery itself is unavailable. + } +} + +function createStagehand(initAttemptId: string): Stagehand { const { apiKey, model, proxies, sessionTimeoutSeconds } = extension.config; return new Stagehand({ @@ -47,6 +77,9 @@ function createStagehand(): Stagehand { keepAlive: true, timeout: sessionTimeoutSeconds, proxies, + userMetadata: { + eve: { initAttemptId }, + }, }, }); } @@ -73,7 +106,7 @@ async function connect(): Promise { await resumed.init(); return { stagehand: resumed, created: false }; } catch (error) { - await resumed.close().catch(() => {}); + await disconnectStagehand(resumed); if ((await getRemoteSessionState(current.id)) !== 'terminal') { throw error; @@ -83,8 +116,15 @@ async function connect(): Promise { } } - const created = createStagehand(); - await created.init(); + const initAttemptId = randomUUID(); + const created = createStagehand(initAttemptId); + try { + await created.init(); + } catch (error) { + await persistPartiallyCreatedSession(initAttemptId); + await disconnectStagehand(created); + throw error; + } browserSession.update(() => ({ id: created.browserbaseSessionID ?? null, url: created.browserbaseSessionURL ?? null, @@ -101,7 +141,7 @@ export async function withStagehand( try { return await operation(stagehand); } finally { - await stagehand.close(); + await disconnectStagehand(stagehand); } }); } @@ -114,7 +154,7 @@ export async function createBrowserSession( try { return { ...browserSession.get(), created }; } finally { - await stagehand.close(); + await disconnectStagehand(stagehand); } }); } diff --git a/examples/integrations/vercel/eve/tests/stagehand.test.ts b/examples/integrations/vercel/eve/tests/stagehand.test.ts new file mode 100644 index 0000000..913ec27 --- /dev/null +++ b/examples/integrations/vercel/eve/tests/stagehand.test.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { disconnectStagehand } from '../extension/lib/disconnect.ts'; + +test('does not surface a Stagehand disconnect failure', async () => { + await assert.doesNotReject( + disconnectStagehand({ + async close() { + throw new Error('disconnect failed'); + }, + }) + ); +}); From e4f015871f55f95b2024c012c152e9f39842d4df Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Tue, 21 Jul 2026 20:45:32 -0700 Subject: [PATCH 5/8] Flatten Eve session recovery metadata --- .../eve/extension/lib/session-metadata.ts | 11 +++++++++++ .../vercel/eve/extension/lib/stagehand.ts | 10 ++++++---- .../vercel/eve/tests/session-metadata.test.ts | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 examples/integrations/vercel/eve/extension/lib/session-metadata.ts create mode 100644 examples/integrations/vercel/eve/tests/session-metadata.test.ts diff --git a/examples/integrations/vercel/eve/extension/lib/session-metadata.ts b/examples/integrations/vercel/eve/extension/lib/session-metadata.ts new file mode 100644 index 0000000..522c394 --- /dev/null +++ b/examples/integrations/vercel/eve/extension/lib/session-metadata.ts @@ -0,0 +1,11 @@ +const INIT_ATTEMPT_METADATA_KEY = 'eveInitAttemptId'; + +export function createInitAttemptMetadata( + initAttemptId: string +): Record { + return { [INIT_ATTEMPT_METADATA_KEY]: initAttemptId }; +} + +export function createInitAttemptQuery(initAttemptId: string): string { + return `user_metadata['${INIT_ATTEMPT_METADATA_KEY}']:'${initAttemptId}'`; +} diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/examples/integrations/vercel/eve/extension/lib/stagehand.ts index ca858e7..c5593e3 100644 --- a/examples/integrations/vercel/eve/extension/lib/stagehand.ts +++ b/examples/integrations/vercel/eve/extension/lib/stagehand.ts @@ -6,6 +6,10 @@ import { APIError } from '@browserbasehq/sdk'; import extension from '../extension'; import { createBrowserbaseClient } from './browserbase'; import { disconnectStagehand } from './disconnect'; +import { + createInitAttemptMetadata, + createInitAttemptQuery, +} from './session-metadata'; import { withSessionLock } from './session-lock'; import { browserSession, type BrowserSessionState } from './session-state'; @@ -47,7 +51,7 @@ async function persistPartiallyCreatedSession( ): Promise { try { const sessions = await createBrowserbaseClient().sessions.list({ - q: `user_metadata['eve']['initAttemptId']:'${initAttemptId}'`, + q: createInitAttemptQuery(initAttemptId), }); const active = sessions.find(session => ACTIVE_SESSION_STATUSES.has(session.status) @@ -77,9 +81,7 @@ function createStagehand(initAttemptId: string): Stagehand { keepAlive: true, timeout: sessionTimeoutSeconds, proxies, - userMetadata: { - eve: { initAttemptId }, - }, + userMetadata: createInitAttemptMetadata(initAttemptId), }, }); } diff --git a/examples/integrations/vercel/eve/tests/session-metadata.test.ts b/examples/integrations/vercel/eve/tests/session-metadata.test.ts new file mode 100644 index 0000000..b40a04c --- /dev/null +++ b/examples/integrations/vercel/eve/tests/session-metadata.test.ts @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + createInitAttemptMetadata, + createInitAttemptQuery, +} from '../extension/lib/session-metadata.ts'; + +test('uses one flat string field for failed-init session recovery', () => { + const initAttemptId = 'attempt-123'; + + assert.deepEqual(createInitAttemptMetadata(initAttemptId), { + eveInitAttemptId: initAttemptId, + }); + assert.equal( + createInitAttemptQuery(initAttemptId), + "user_metadata['eveInitAttemptId']:'attempt-123'" + ); +}); From 54cfc5214345c78a49d2ec93e87b6702d2fe7416 Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Tue, 21 Jul 2026 20:52:08 -0700 Subject: [PATCH 6/8] Release Eve browser sessions through Browserbase --- .../eve/extension/lib/release-session.ts | 76 +++++++++++++++++++ .../vercel/eve/extension/lib/stagehand.ts | 52 +++---------- .../vercel/eve/tests/release-session.test.ts | 54 +++++++++++++ 3 files changed, 141 insertions(+), 41 deletions(-) create mode 100644 examples/integrations/vercel/eve/extension/lib/release-session.ts create mode 100644 examples/integrations/vercel/eve/tests/release-session.test.ts diff --git a/examples/integrations/vercel/eve/extension/lib/release-session.ts b/examples/integrations/vercel/eve/extension/lib/release-session.ts new file mode 100644 index 0000000..a4b362a --- /dev/null +++ b/examples/integrations/vercel/eve/extension/lib/release-session.ts @@ -0,0 +1,76 @@ +import { APIError } from '@browserbasehq/sdk'; + +type BrowserbaseSessionStatus = + | 'PENDING' + | 'RUNNING' + | 'ERROR' + | 'TIMED_OUT' + | 'COMPLETED'; + +interface BrowserbaseSessionRecord { + status: BrowserbaseSessionStatus; +} + +export interface BrowserbaseSessionApi { + retrieve(sessionId: string): Promise; + update( + sessionId: string, + body: { status: 'REQUEST_RELEASE' } + ): Promise; +} + +export type RemoteSessionState = 'active' | 'terminal' | 'unknown'; + +const RELEASE_POLL_DELAYS_MS = [100, 250, 500, 1_000] as const; + +export function isActiveSessionStatus( + status: BrowserbaseSessionStatus +): boolean { + return status === 'PENDING' || status === 'RUNNING'; +} + +export async function getRemoteSessionState( + sessions: Pick, + sessionId: string +): Promise { + try { + const session = await sessions.retrieve(sessionId); + return isActiveSessionStatus(session.status) ? 'active' : 'terminal'; + } catch (error) { + return error instanceof APIError && error.status === 404 + ? 'terminal' + : 'unknown'; + } +} + +export async function releaseBrowserbaseSession( + sessions: BrowserbaseSessionApi, + sessionId: string, + wait: (milliseconds: number) => Promise = milliseconds => + new Promise(resolve => setTimeout(resolve, milliseconds)) +): Promise { + try { + const release = await sessions.update(sessionId, { + status: 'REQUEST_RELEASE', + }); + if (!isActiveSessionStatus(release.status)) return; + } catch (error) { + if (error instanceof APIError && error.status === 404) return; + throw error; + } + + for (const delay of RELEASE_POLL_DELAYS_MS) { + await wait(delay); + const state = await getRemoteSessionState(sessions, sessionId); + if (state === 'terminal') return; + if (state === 'unknown') { + throw new Error( + `Unable to confirm Browserbase session ${sessionId} was released.` + ); + } + } + + throw new Error( + `Browserbase session ${sessionId} is still active after requesting release.` + ); +} diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/examples/integrations/vercel/eve/extension/lib/stagehand.ts index c5593e3..a18ace9 100644 --- a/examples/integrations/vercel/eve/extension/lib/stagehand.ts +++ b/examples/integrations/vercel/eve/extension/lib/stagehand.ts @@ -1,11 +1,15 @@ import { randomUUID } from 'node:crypto'; import { Stagehand } from '@browserbasehq/stagehand'; -import { APIError } from '@browserbasehq/sdk'; import extension from '../extension'; import { createBrowserbaseClient } from './browserbase'; import { disconnectStagehand } from './disconnect'; +import { + getRemoteSessionState, + isActiveSessionStatus, + releaseBrowserbaseSession, +} from './release-session'; import { createInitAttemptMetadata, createInitAttemptQuery, @@ -24,28 +28,10 @@ export interface BrowserSessionResult extends BrowserSessionState { created: boolean; } -type RemoteSessionState = 'active' | 'terminal' | 'unknown'; -const ACTIVE_SESSION_STATUSES = new Set(['PENDING', 'RUNNING']); - function browserbaseSessionUrl(sessionId: string): string { return `https://www.browserbase.com/sessions/${sessionId}`; } -async function getRemoteSessionState( - sessionId: string -): Promise { - try { - const session = await createBrowserbaseClient().sessions.retrieve(sessionId); - return ACTIVE_SESSION_STATUSES.has(session.status) - ? 'active' - : 'terminal'; - } catch (error) { - return error instanceof APIError && error.status === 404 - ? 'terminal' - : 'unknown'; - } -} - async function persistPartiallyCreatedSession( initAttemptId: string ): Promise { @@ -54,7 +40,7 @@ async function persistPartiallyCreatedSession( q: createInitAttemptQuery(initAttemptId), }); const active = sessions.find(session => - ACTIVE_SESSION_STATUSES.has(session.status) + isActiveSessionStatus(session.status) ); if (!active) return; @@ -110,7 +96,8 @@ async function connect(): Promise { } catch (error) { await disconnectStagehand(resumed); - if ((await getRemoteSessionState(current.id)) !== 'terminal') { + const sessions = createBrowserbaseClient().sessions; + if ((await getRemoteSessionState(sessions, current.id)) !== 'terminal') { throw error; } @@ -168,30 +155,13 @@ export async function closeBrowserSession( const current = browserSession.get(); if (!current.id) return current; - if ((await getRemoteSessionState(current.id)) === 'terminal') { + const sessions = createBrowserbaseClient().sessions; + if ((await getRemoteSessionState(sessions, current.id)) === 'terminal') { browserSession.update(() => ({ id: null, url: null })); return current; } - const stagehand = new Stagehand({ - env: 'BROWSERBASE', - apiKey: extension.config.apiKey, - browserbaseSessionID: current.id, - disablePino: true, - // Eve bundles Stagehand without its CLI-only crash supervisor entrypoint. - // This path closes the session explicitly, so supervisor logging is not useful. - logger: () => {}, - keepAlive: false, - }); - - try { - await stagehand.init(); - await stagehand.close(); - } catch (error) { - if ((await getRemoteSessionState(current.id)) !== 'terminal') { - throw error; - } - } + await releaseBrowserbaseSession(sessions, current.id); browserSession.update(() => ({ id: null, url: null })); return current; diff --git a/examples/integrations/vercel/eve/tests/release-session.test.ts b/examples/integrations/vercel/eve/tests/release-session.test.ts new file mode 100644 index 0000000..9129790 --- /dev/null +++ b/examples/integrations/vercel/eve/tests/release-session.test.ts @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + type BrowserbaseSessionApi, + releaseBrowserbaseSession, +} from '../extension/lib/release-session.ts'; + +function sessionApi(options: { + retrieve: BrowserbaseSessionApi['retrieve']; + update: BrowserbaseSessionApi['update']; +}): BrowserbaseSessionApi { + return options; +} + +test('requests release and waits for a terminal Browserbase session', async () => { + const updates: string[] = []; + const waits: number[] = []; + let retrievals = 0; + const sessions = sessionApi({ + async update(sessionId, body) { + assert.deepEqual(body, { status: 'REQUEST_RELEASE' }); + updates.push(sessionId); + return { status: 'RUNNING' }; + }, + async retrieve() { + retrievals += 1; + return { status: retrievals === 1 ? 'RUNNING' : 'COMPLETED' }; + }, + }); + + await releaseBrowserbaseSession(sessions, 'session-123', async milliseconds => { + waits.push(milliseconds); + }); + + assert.deepEqual(updates, ['session-123']); + assert.deepEqual(waits, [100, 250]); +}); + +test('does not report success while the Browserbase session remains active', async () => { + const sessions = sessionApi({ + async update() { + return { status: 'RUNNING' }; + }, + async retrieve() { + return { status: 'RUNNING' }; + }, + }); + + await assert.rejects( + releaseBrowserbaseSession(sessions, 'session-123', async () => {}), + /still active/ + ); +}); From 23ba3241271969b5dfefc98fff4ea876b91476c8 Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Tue, 21 Jul 2026 20:58:08 -0700 Subject: [PATCH 7/8] Simplify Eve session lifecycle --- .../eve/extension/lib/release-session.ts | 4 +- .../eve/extension/lib/session-metadata.ts | 11 --- .../vercel/eve/extension/lib/stagehand.ts | 40 +---------- examples/integrations/vercel/eve/package.json | 3 +- .../vercel/eve/tests/release-session.test.ts | 54 --------------- .../vercel/eve/tests/session-lock.test.ts | 69 ------------------- .../vercel/eve/tests/session-metadata.test.ts | 19 ----- .../vercel/eve/tests/stagehand.test.ts | 14 ---- .../integrations/vercel/eve/tsconfig.json | 3 +- 9 files changed, 6 insertions(+), 211 deletions(-) delete mode 100644 examples/integrations/vercel/eve/extension/lib/session-metadata.ts delete mode 100644 examples/integrations/vercel/eve/tests/release-session.test.ts delete mode 100644 examples/integrations/vercel/eve/tests/session-lock.test.ts delete mode 100644 examples/integrations/vercel/eve/tests/session-metadata.test.ts delete mode 100644 examples/integrations/vercel/eve/tests/stagehand.test.ts diff --git a/examples/integrations/vercel/eve/extension/lib/release-session.ts b/examples/integrations/vercel/eve/extension/lib/release-session.ts index a4b362a..f85fa5a 100644 --- a/examples/integrations/vercel/eve/extension/lib/release-session.ts +++ b/examples/integrations/vercel/eve/extension/lib/release-session.ts @@ -11,7 +11,7 @@ interface BrowserbaseSessionRecord { status: BrowserbaseSessionStatus; } -export interface BrowserbaseSessionApi { +interface BrowserbaseSessionApi { retrieve(sessionId: string): Promise; update( sessionId: string, @@ -23,7 +23,7 @@ export type RemoteSessionState = 'active' | 'terminal' | 'unknown'; const RELEASE_POLL_DELAYS_MS = [100, 250, 500, 1_000] as const; -export function isActiveSessionStatus( +function isActiveSessionStatus( status: BrowserbaseSessionStatus ): boolean { return status === 'PENDING' || status === 'RUNNING'; diff --git a/examples/integrations/vercel/eve/extension/lib/session-metadata.ts b/examples/integrations/vercel/eve/extension/lib/session-metadata.ts deleted file mode 100644 index 522c394..0000000 --- a/examples/integrations/vercel/eve/extension/lib/session-metadata.ts +++ /dev/null @@ -1,11 +0,0 @@ -const INIT_ATTEMPT_METADATA_KEY = 'eveInitAttemptId'; - -export function createInitAttemptMetadata( - initAttemptId: string -): Record { - return { [INIT_ATTEMPT_METADATA_KEY]: initAttemptId }; -} - -export function createInitAttemptQuery(initAttemptId: string): string { - return `user_metadata['${INIT_ATTEMPT_METADATA_KEY}']:'${initAttemptId}'`; -} diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/examples/integrations/vercel/eve/extension/lib/stagehand.ts index a18ace9..de427fe 100644 --- a/examples/integrations/vercel/eve/extension/lib/stagehand.ts +++ b/examples/integrations/vercel/eve/extension/lib/stagehand.ts @@ -1,5 +1,3 @@ -import { randomUUID } from 'node:crypto'; - import { Stagehand } from '@browserbasehq/stagehand'; import extension from '../extension'; @@ -7,13 +5,8 @@ import { createBrowserbaseClient } from './browserbase'; import { disconnectStagehand } from './disconnect'; import { getRemoteSessionState, - isActiveSessionStatus, releaseBrowserbaseSession, } from './release-session'; -import { - createInitAttemptMetadata, - createInitAttemptQuery, -} from './session-metadata'; import { withSessionLock } from './session-lock'; import { browserSession, type BrowserSessionState } from './session-state'; @@ -28,33 +21,7 @@ export interface BrowserSessionResult extends BrowserSessionState { created: boolean; } -function browserbaseSessionUrl(sessionId: string): string { - return `https://www.browserbase.com/sessions/${sessionId}`; -} - -async function persistPartiallyCreatedSession( - initAttemptId: string -): Promise { - try { - const sessions = await createBrowserbaseClient().sessions.list({ - q: createInitAttemptQuery(initAttemptId), - }); - const active = sessions.find(session => - isActiveSessionStatus(session.status) - ); - if (!active) return; - - browserSession.update(() => ({ - id: active.id, - url: browserbaseSessionUrl(active.id), - })); - } catch { - // Preserve the original Stagehand init error. The attempt metadata remains - // attached in Browserbase for diagnosis if recovery itself is unavailable. - } -} - -function createStagehand(initAttemptId: string): Stagehand { +function createStagehand(): Stagehand { const { apiKey, model, proxies, sessionTimeoutSeconds } = extension.config; return new Stagehand({ @@ -67,7 +34,6 @@ function createStagehand(initAttemptId: string): Stagehand { keepAlive: true, timeout: sessionTimeoutSeconds, proxies, - userMetadata: createInitAttemptMetadata(initAttemptId), }, }); } @@ -105,12 +71,10 @@ async function connect(): Promise { } } - const initAttemptId = randomUUID(); - const created = createStagehand(initAttemptId); + const created = createStagehand(); try { await created.init(); } catch (error) { - await persistPartiallyCreatedSession(initAttemptId); await disconnectStagehand(created); throw error; } diff --git a/examples/integrations/vercel/eve/package.json b/examples/integrations/vercel/eve/package.json index 38bdf35..bba8e8d 100644 --- a/examples/integrations/vercel/eve/package.json +++ b/examples/integrations/vercel/eve/package.json @@ -36,8 +36,7 @@ "build": "eve extension build", "prepare": "eve extension build", "typecheck": "tsc", - "check": "pnpm typecheck && pnpm test && pnpm build", - "test": "node --test tests/*.test.ts" + "check": "pnpm typecheck && pnpm build" }, "dependencies": { "@browserbasehq/sdk": "2.16.0", diff --git a/examples/integrations/vercel/eve/tests/release-session.test.ts b/examples/integrations/vercel/eve/tests/release-session.test.ts deleted file mode 100644 index 9129790..0000000 --- a/examples/integrations/vercel/eve/tests/release-session.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { - type BrowserbaseSessionApi, - releaseBrowserbaseSession, -} from '../extension/lib/release-session.ts'; - -function sessionApi(options: { - retrieve: BrowserbaseSessionApi['retrieve']; - update: BrowserbaseSessionApi['update']; -}): BrowserbaseSessionApi { - return options; -} - -test('requests release and waits for a terminal Browserbase session', async () => { - const updates: string[] = []; - const waits: number[] = []; - let retrievals = 0; - const sessions = sessionApi({ - async update(sessionId, body) { - assert.deepEqual(body, { status: 'REQUEST_RELEASE' }); - updates.push(sessionId); - return { status: 'RUNNING' }; - }, - async retrieve() { - retrievals += 1; - return { status: retrievals === 1 ? 'RUNNING' : 'COMPLETED' }; - }, - }); - - await releaseBrowserbaseSession(sessions, 'session-123', async milliseconds => { - waits.push(milliseconds); - }); - - assert.deepEqual(updates, ['session-123']); - assert.deepEqual(waits, [100, 250]); -}); - -test('does not report success while the Browserbase session remains active', async () => { - const sessions = sessionApi({ - async update() { - return { status: 'RUNNING' }; - }, - async retrieve() { - return { status: 'RUNNING' }; - }, - }); - - await assert.rejects( - releaseBrowserbaseSession(sessions, 'session-123', async () => {}), - /still active/ - ); -}); diff --git a/examples/integrations/vercel/eve/tests/session-lock.test.ts b/examples/integrations/vercel/eve/tests/session-lock.test.ts deleted file mode 100644 index 450adcc..0000000 --- a/examples/integrations/vercel/eve/tests/session-lock.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { withSessionLock } from '../extension/lib/session-lock.ts'; - -test('serializes operations within one Eve session', async () => { - const events: string[] = []; - let releaseFirst = () => {}; - const firstBlocked = new Promise(resolve => { - releaseFirst = resolve; - }); - - const first = withSessionLock('session-a', async () => { - events.push('first:start'); - await firstBlocked; - events.push('first:end'); - }); - const second = withSessionLock('session-a', async () => { - events.push('second:start'); - events.push('second:end'); - }); - - await new Promise(resolve => setImmediate(resolve)); - assert.deepEqual(events, ['first:start']); - - releaseFirst(); - await Promise.all([first, second]); - assert.deepEqual(events, [ - 'first:start', - 'first:end', - 'second:start', - 'second:end', - ]); -}); - -test('does not serialize operations from different Eve sessions', async () => { - const started: string[] = []; - let release = () => {}; - const blocked = new Promise(resolve => { - release = resolve; - }); - - const first = withSessionLock('session-a', async () => { - started.push('session-a'); - await blocked; - }); - const second = withSessionLock('session-b', async () => { - started.push('session-b'); - await blocked; - }); - - await new Promise(resolve => setImmediate(resolve)); - assert.deepEqual(started.sort(), ['session-a', 'session-b']); - - release(); - await Promise.all([first, second]); -}); - -test('releases a session lock when an operation fails', async () => { - await assert.rejects( - withSessionLock('session-a', async () => { - throw new Error('expected failure'); - }), - /expected failure/ - ); - - const result = await withSessionLock('session-a', async () => 'recovered'); - assert.equal(result, 'recovered'); -}); diff --git a/examples/integrations/vercel/eve/tests/session-metadata.test.ts b/examples/integrations/vercel/eve/tests/session-metadata.test.ts deleted file mode 100644 index b40a04c..0000000 --- a/examples/integrations/vercel/eve/tests/session-metadata.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { - createInitAttemptMetadata, - createInitAttemptQuery, -} from '../extension/lib/session-metadata.ts'; - -test('uses one flat string field for failed-init session recovery', () => { - const initAttemptId = 'attempt-123'; - - assert.deepEqual(createInitAttemptMetadata(initAttemptId), { - eveInitAttemptId: initAttemptId, - }); - assert.equal( - createInitAttemptQuery(initAttemptId), - "user_metadata['eveInitAttemptId']:'attempt-123'" - ); -}); diff --git a/examples/integrations/vercel/eve/tests/stagehand.test.ts b/examples/integrations/vercel/eve/tests/stagehand.test.ts deleted file mode 100644 index 913ec27..0000000 --- a/examples/integrations/vercel/eve/tests/stagehand.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { disconnectStagehand } from '../extension/lib/disconnect.ts'; - -test('does not surface a Stagehand disconnect failure', async () => { - await assert.doesNotReject( - disconnectStagehand({ - async close() { - throw new Error('disconnect failed'); - }, - }) - ); -}); diff --git a/examples/integrations/vercel/eve/tsconfig.json b/examples/integrations/vercel/eve/tsconfig.json index 5174a94..ba1682b 100644 --- a/examples/integrations/vercel/eve/tsconfig.json +++ b/examples/integrations/vercel/eve/tsconfig.json @@ -3,12 +3,11 @@ "target": "ES2022", "module": "esnext", "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "types": ["node"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "noEmit": true }, - "include": ["extension/**/*.ts", "tests/**/*.ts"] + "include": ["extension/**/*.ts"] } From a84b82a673670850b4bf89d19244e6dddfee548e Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Wed, 22 Jul 2026 12:44:06 -0700 Subject: [PATCH 8/8] refactor: move eve extension into packages --- examples/integrations/vercel/README.md | 9 ++-- .../integrations/vercel/eve-example/README.md | 11 ++-- .../vercel/eve/pnpm-workspace.yaml | 3 -- .../eve-browserbase}/.gitignore | 0 .../eve => packages/eve-browserbase}/.nvmrc | 0 .../eve-browserbase}/README.md | 51 ++++++++++--------- .../eve-browserbase}/extension/extension.ts | 0 .../extension/instructions/browser.md | 0 .../extension/lib/browserbase.ts | 0 .../extension/lib/disconnect.ts | 0 .../extension/lib/release-session.ts | 0 .../extension/lib/session-lock.ts | 0 .../extension/lib/session-state.ts | 0 .../extension/lib/stagehand.ts | 0 .../eve-browserbase}/extension/tools/act.ts | 0 .../eve-browserbase}/extension/tools/agent.ts | 0 .../extension/tools/extract.ts | 0 .../eve-browserbase}/extension/tools/fetch.ts | 0 .../extension/tools/navigate.ts | 0 .../extension/tools/observe.ts | 0 .../extension/tools/search.ts | 0 .../extension/tools/session.ts | 0 .../eve-browserbase}/package.json | 0 packages/eve-browserbase/pnpm-workspace.yaml | 3 ++ .../eve-browserbase}/tsconfig.json | 0 25 files changed, 40 insertions(+), 37 deletions(-) delete mode 100644 examples/integrations/vercel/eve/pnpm-workspace.yaml rename {examples/integrations/vercel/eve => packages/eve-browserbase}/.gitignore (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/.nvmrc (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/README.md (79%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/extension.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/instructions/browser.md (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/lib/browserbase.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/lib/disconnect.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/lib/release-session.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/lib/session-lock.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/lib/session-state.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/lib/stagehand.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/act.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/agent.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/extract.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/fetch.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/navigate.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/observe.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/search.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/extension/tools/session.ts (100%) rename {examples/integrations/vercel/eve => packages/eve-browserbase}/package.json (100%) create mode 100644 packages/eve-browserbase/pnpm-workspace.yaml rename {examples/integrations/vercel/eve => packages/eve-browserbase}/tsconfig.json (100%) diff --git a/examples/integrations/vercel/README.md b/examples/integrations/vercel/README.md index ca5ce4c..5457c61 100644 --- a/examples/integrations/vercel/README.md +++ b/examples/integrations/vercel/README.md @@ -20,7 +20,8 @@ BrowseGPT is a chat interface that allows users to search the web and get answer The second example shows how to use Puppeteer with Vercel on Fluid Compute. It demonstrates setting up a Next.js application with Puppeteer for browser automation tasks, deployed on Vercel's Fluid Compute infrastructure which provides longer execution times needed for browser automation. -The [Eve extension](./eve/) gives Eve agents a persistent Browserbase browser -through Stagehand's `act`, `observe`, `extract`, and autonomous agent primitives. -The sibling [example agent](./eve-example/) mounts the extension in a runnable -pnpm workspace. +The [`@browserbasehq/eve`](../../../packages/eve-browserbase/) package gives Eve +agents a persistent Browserbase browser through Stagehand's `act`, `observe`, +`extract`, and autonomous agent primitives. The +[example agent](./eve-example/) mounts the extension in a runnable pnpm +workspace. diff --git a/examples/integrations/vercel/eve-example/README.md b/examples/integrations/vercel/eve-example/README.md index db20870..ca31cc9 100644 --- a/examples/integrations/vercel/eve-example/README.md +++ b/examples/integrations/vercel/eve-example/README.md @@ -9,17 +9,18 @@ its tools are available as `browserbase__create_session`, ## Run it -From the sibling `eve` extension directory: +From the repository root, enter the `eve` package directory: ```bash -cd ../eve +cd packages/eve-browserbase nvm use pnpm install pnpm build -cp ../eve-example/.env.example ../eve-example/.env +cp ../../examples/integrations/vercel/eve-example/.env.example ../../examples/integrations/vercel/eve-example/.env ``` -Fill in the values in `../eve-example/.env`, then start the agent: +Fill in the values in `../../examples/integrations/vercel/eve-example/.env`, +then start the agent from the same `packages/eve-browserbase` directory: ```bash pnpm --filter browserbase-eve-example dev @@ -44,5 +45,5 @@ The environment requires: `VERCEL_OIDC_TOKEN` can be used instead. The dependency on `@browserbasehq/eve` uses pnpm's `workspace:*` protocol, so -the example exercises the sibling extension without publishing it first. The +the example exercises the local package without publishing it first. The workspace-level install provides dependencies for both packages. diff --git a/examples/integrations/vercel/eve/pnpm-workspace.yaml b/examples/integrations/vercel/eve/pnpm-workspace.yaml deleted file mode 100644 index 0c0544b..0000000 --- a/examples/integrations/vercel/eve/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - - '.' - - '../eve-example' diff --git a/examples/integrations/vercel/eve/.gitignore b/packages/eve-browserbase/.gitignore similarity index 100% rename from examples/integrations/vercel/eve/.gitignore rename to packages/eve-browserbase/.gitignore diff --git a/examples/integrations/vercel/eve/.nvmrc b/packages/eve-browserbase/.nvmrc similarity index 100% rename from examples/integrations/vercel/eve/.nvmrc rename to packages/eve-browserbase/.nvmrc diff --git a/examples/integrations/vercel/eve/README.md b/packages/eve-browserbase/README.md similarity index 79% rename from examples/integrations/vercel/eve/README.md rename to packages/eve-browserbase/README.md index 593d9b9..7e142d8 100644 --- a/examples/integrations/vercel/eve/README.md +++ b/packages/eve-browserbase/README.md @@ -38,7 +38,7 @@ export default browserbase({ Start Eve and give the agent a browser task: ```bash -npx eve dev +pnpm exec eve dev ``` ```text @@ -47,23 +47,24 @@ five stories. ``` The extension keeps the Browserbase session ID in Eve's durable per-session -state. Each tool reconnects to that browser, performs one operation, and -disconnects without terminating it. `browserbase__create_session` creates or -reconnects the browser explicitly, and `browserbase__stop_session` terminates it. +state. Each browser tool reconnects to that browser, performs one operation, +and disconnects without terminating it. `browserbase__create_session` creates +or reconnects the browser explicitly, and `browserbase__stop_session` +terminates it. ## Tools -| Tool | Purpose | -| ---------------- | --------------------------------------------------------- | -| `search` | Find relevant public web pages with Browserbase Search. | -| `fetch` | Retrieve raw, markdown, or structured page content. | -| `create_session` | Create or reconnect the Browserbase session. | -| `stop_session` | Stop the Browserbase session and release resources. | -| `navigate` | Open a URL in the current browser. | -| `observe` | Find relevant elements and candidate actions. | -| `act` | Perform one natural-language page interaction. | -| `extract` | Return data validated against a supplied JSON Schema. | -| `agent` | Run a multi-step autonomous Stagehand task. | +| Tool | Purpose | +| ---------------- | ------------------------------------------------------- | +| `search` | Find relevant public web pages with Browserbase Search. | +| `fetch` | Retrieve raw, markdown, or structured page content. | +| `create_session` | Create or reconnect the Browserbase session. | +| `stop_session` | Stop the Browserbase session and release resources. | +| `navigate` | Open a URL in the current browser. | +| `observe` | Find relevant elements and candidate actions. | +| `act` | Perform one natural-language page interaction. | +| `extract` | Return data validated against a supplied JSON Schema. | +| `agent` | Run a multi-step autonomous Stagehand task. | Use Search → Fetch → browser as an escalation path: discover sources cheaply, retrieve straightforward content without a session, and create a browser only @@ -76,12 +77,12 @@ workflows that need Stagehand to plan several steps on its own. ## Configuration -| Option | Default | Description | -| ----------------------- | --------------------- | ------------------------------------------------------- | -| `apiKey` | required | Browserbase API key for browsers and Model Gateway. | -| `model` | `openai/gpt-5.4-mini` | Stagehand Model Gateway model identifier. | -| `sessionTimeoutSeconds` | `900` | Session timeout, from 60 to 21,600 seconds. | -| `proxies` | `false` | Enable Browserbase proxies for new sessions. | +| Option | Default | Description | +| ----------------------- | --------------------- | --------------------------------------------------- | +| `apiKey` | required | Browserbase API key for browsers and Model Gateway. | +| `model` | `openai/gpt-5.4-mini` | Stagehand Model Gateway model identifier. | +| `sessionTimeoutSeconds` | `900` | Session timeout, from 60 to 21,600 seconds. | +| `proxies` | `false` | Enable Browserbase proxies for new sessions. | Stagehand runs through Browserbase Model Gateway, so consumers do not need an OpenAI or other model-provider API key. The Browserbase API key covers both the @@ -110,7 +111,7 @@ run `nvm install 24.16.0` first if needed. ## Example agent -The sibling [`eve-example`](../eve-example/) directory contains a runnable Eve -agent that mounts this package locally. It includes the agent instructions, -environment template, and a Browserbase research prompt for a complete smoke -test. +The [`eve-example`](../../examples/integrations/vercel/eve-example/) directory +contains a runnable Eve agent that mounts this package locally. It includes the +agent instructions, environment template, and a Browserbase research prompt for +a complete smoke test. diff --git a/examples/integrations/vercel/eve/extension/extension.ts b/packages/eve-browserbase/extension/extension.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/extension.ts rename to packages/eve-browserbase/extension/extension.ts diff --git a/examples/integrations/vercel/eve/extension/instructions/browser.md b/packages/eve-browserbase/extension/instructions/browser.md similarity index 100% rename from examples/integrations/vercel/eve/extension/instructions/browser.md rename to packages/eve-browserbase/extension/instructions/browser.md diff --git a/examples/integrations/vercel/eve/extension/lib/browserbase.ts b/packages/eve-browserbase/extension/lib/browserbase.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/lib/browserbase.ts rename to packages/eve-browserbase/extension/lib/browserbase.ts diff --git a/examples/integrations/vercel/eve/extension/lib/disconnect.ts b/packages/eve-browserbase/extension/lib/disconnect.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/lib/disconnect.ts rename to packages/eve-browserbase/extension/lib/disconnect.ts diff --git a/examples/integrations/vercel/eve/extension/lib/release-session.ts b/packages/eve-browserbase/extension/lib/release-session.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/lib/release-session.ts rename to packages/eve-browserbase/extension/lib/release-session.ts diff --git a/examples/integrations/vercel/eve/extension/lib/session-lock.ts b/packages/eve-browserbase/extension/lib/session-lock.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/lib/session-lock.ts rename to packages/eve-browserbase/extension/lib/session-lock.ts diff --git a/examples/integrations/vercel/eve/extension/lib/session-state.ts b/packages/eve-browserbase/extension/lib/session-state.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/lib/session-state.ts rename to packages/eve-browserbase/extension/lib/session-state.ts diff --git a/examples/integrations/vercel/eve/extension/lib/stagehand.ts b/packages/eve-browserbase/extension/lib/stagehand.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/lib/stagehand.ts rename to packages/eve-browserbase/extension/lib/stagehand.ts diff --git a/examples/integrations/vercel/eve/extension/tools/act.ts b/packages/eve-browserbase/extension/tools/act.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/act.ts rename to packages/eve-browserbase/extension/tools/act.ts diff --git a/examples/integrations/vercel/eve/extension/tools/agent.ts b/packages/eve-browserbase/extension/tools/agent.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/agent.ts rename to packages/eve-browserbase/extension/tools/agent.ts diff --git a/examples/integrations/vercel/eve/extension/tools/extract.ts b/packages/eve-browserbase/extension/tools/extract.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/extract.ts rename to packages/eve-browserbase/extension/tools/extract.ts diff --git a/examples/integrations/vercel/eve/extension/tools/fetch.ts b/packages/eve-browserbase/extension/tools/fetch.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/fetch.ts rename to packages/eve-browserbase/extension/tools/fetch.ts diff --git a/examples/integrations/vercel/eve/extension/tools/navigate.ts b/packages/eve-browserbase/extension/tools/navigate.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/navigate.ts rename to packages/eve-browserbase/extension/tools/navigate.ts diff --git a/examples/integrations/vercel/eve/extension/tools/observe.ts b/packages/eve-browserbase/extension/tools/observe.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/observe.ts rename to packages/eve-browserbase/extension/tools/observe.ts diff --git a/examples/integrations/vercel/eve/extension/tools/search.ts b/packages/eve-browserbase/extension/tools/search.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/search.ts rename to packages/eve-browserbase/extension/tools/search.ts diff --git a/examples/integrations/vercel/eve/extension/tools/session.ts b/packages/eve-browserbase/extension/tools/session.ts similarity index 100% rename from examples/integrations/vercel/eve/extension/tools/session.ts rename to packages/eve-browserbase/extension/tools/session.ts diff --git a/examples/integrations/vercel/eve/package.json b/packages/eve-browserbase/package.json similarity index 100% rename from examples/integrations/vercel/eve/package.json rename to packages/eve-browserbase/package.json diff --git a/packages/eve-browserbase/pnpm-workspace.yaml b/packages/eve-browserbase/pnpm-workspace.yaml new file mode 100644 index 0000000..1d91c48 --- /dev/null +++ b/packages/eve-browserbase/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - '.' + - '../../examples/integrations/vercel/eve-example' diff --git a/examples/integrations/vercel/eve/tsconfig.json b/packages/eve-browserbase/tsconfig.json similarity index 100% rename from examples/integrations/vercel/eve/tsconfig.json rename to packages/eve-browserbase/tsconfig.json