diff --git a/examples/integrations/vercel/README.md b/examples/integrations/vercel/README.md index 40ea742..5457c61 100644 --- a/examples/integrations/vercel/README.md +++ b/examples/integrations/vercel/README.md @@ -14,8 +14,14 @@ 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 [`@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/.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..ca31cc9 --- /dev/null +++ b/examples/integrations/vercel/eve-example/README.md @@ -0,0 +1,49 @@ +# 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 repository root, enter the `eve` package directory: + +```bash +cd packages/eve-browserbase +nvm use +pnpm install +pnpm build +cp ../../examples/integrations/vercel/eve-example/.env.example ../../examples/integrations/vercel/eve-example/.env +``` + +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 +``` + +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 local package 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/packages/eve-browserbase/.gitignore b/packages/eve-browserbase/.gitignore new file mode 100644 index 0000000..95931dd --- /dev/null +++ b/packages/eve-browserbase/.gitignore @@ -0,0 +1,8 @@ +node_modules +.env* +.eve +.vercel +.output +.nitro +dist +*.tsbuildinfo diff --git a/packages/eve-browserbase/.nvmrc b/packages/eve-browserbase/.nvmrc new file mode 100644 index 0000000..b832e40 --- /dev/null +++ b/packages/eve-browserbase/.nvmrc @@ -0,0 +1 @@ +24.16.0 diff --git a/packages/eve-browserbase/README.md b/packages/eve-browserbase/README.md new file mode 100644 index 0000000..7e142d8 --- /dev/null +++ b/packages/eve-browserbase/README.md @@ -0,0 +1,117 @@ +# 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 +pnpm exec 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 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. | + +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. 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 + +```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 [`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/packages/eve-browserbase/extension/extension.ts b/packages/eve-browserbase/extension/extension.ts new file mode 100644 index 0000000..411177d --- /dev/null +++ b/packages/eve-browserbase/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/packages/eve-browserbase/extension/instructions/browser.md b/packages/eve-browserbase/extension/instructions/browser.md new file mode 100644 index 0000000..5e18a10 --- /dev/null +++ b/packages/eve-browserbase/extension/instructions/browser.md @@ -0,0 +1,9 @@ +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. 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/packages/eve-browserbase/extension/lib/browserbase.ts b/packages/eve-browserbase/extension/lib/browserbase.ts new file mode 100644 index 0000000..0c5886f --- /dev/null +++ b/packages/eve-browserbase/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/packages/eve-browserbase/extension/lib/disconnect.ts b/packages/eve-browserbase/extension/lib/disconnect.ts new file mode 100644 index 0000000..52d23f2 --- /dev/null +++ b/packages/eve-browserbase/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/packages/eve-browserbase/extension/lib/release-session.ts b/packages/eve-browserbase/extension/lib/release-session.ts new file mode 100644 index 0000000..f85fa5a --- /dev/null +++ b/packages/eve-browserbase/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; +} + +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; + +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/packages/eve-browserbase/extension/lib/session-lock.ts b/packages/eve-browserbase/extension/lib/session-lock.ts new file mode 100644 index 0000000..da297b2 --- /dev/null +++ b/packages/eve-browserbase/extension/lib/session-lock.ts @@ -0,0 +1,27 @@ +// 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( + 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/packages/eve-browserbase/extension/lib/session-state.ts b/packages/eve-browserbase/extension/lib/session-state.ts new file mode 100644 index 0000000..c541e31 --- /dev/null +++ b/packages/eve-browserbase/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/packages/eve-browserbase/extension/lib/stagehand.ts b/packages/eve-browserbase/extension/lib/stagehand.ts new file mode 100644 index 0000000..de427fe --- /dev/null +++ b/packages/eve-browserbase/extension/lib/stagehand.ts @@ -0,0 +1,133 @@ +import { Stagehand } from '@browserbasehq/stagehand'; + +import extension from '../extension'; +import { createBrowserbaseClient } from './browserbase'; +import { disconnectStagehand } from './disconnect'; +import { + getRemoteSessionState, + releaseBrowserbaseSession, +} from './release-session'; +import { withSessionLock } from './session-lock'; +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; +} + +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 { stagehand: resumed, created: false }; + } catch (error) { + await disconnectStagehand(resumed); + + const sessions = createBrowserbaseClient().sessions; + if ((await getRemoteSessionState(sessions, current.id)) !== 'terminal') { + throw error; + } + + browserSession.update(() => ({ id: null, url: null })); + } + } + + const created = createStagehand(); + try { + await created.init(); + } catch (error) { + await disconnectStagehand(created); + throw error; + } + browserSession.update(() => ({ + id: created.browserbaseSessionID ?? null, + url: created.browserbaseSessionURL ?? null, + })); + return { stagehand: created, created: true }; +} + +export async function withStagehand( + sessionId: string, + operation: StagehandOperation +): Promise { + return withSessionLock(sessionId, async () => { + const { stagehand } = await connect(); + try { + return await operation(stagehand); + } finally { + await disconnectStagehand(stagehand); + } + }); +} + +export async function createBrowserSession( + sessionId: string +): Promise { + return withSessionLock(sessionId, async () => { + const { stagehand, created } = await connect(); + try { + return { ...browserSession.get(), created }; + } finally { + await disconnectStagehand(stagehand); + } + }); +} + +export async function closeBrowserSession( + sessionId: string +): Promise { + return withSessionLock(sessionId, async () => { + const current = browserSession.get(); + if (!current.id) return current; + + const sessions = createBrowserbaseClient().sessions; + if ((await getRemoteSessionState(sessions, current.id)) === 'terminal') { + browserSession.update(() => ({ id: null, url: null })); + return current; + } + + await releaseBrowserbaseSession(sessions, current.id); + + browserSession.update(() => ({ id: null, url: null })); + return current; + }); +} diff --git a/packages/eve-browserbase/extension/tools/act.ts b/packages/eve-browserbase/extension/tools/act.ts new file mode 100644 index 0000000..d902fe1 --- /dev/null +++ b/packages/eve-browserbase/extension/tools/act.ts @@ -0,0 +1,22 @@ +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 }, ctx) { + return withStagehand(ctx.session.id, stagehand => + stagehand.act(instruction) + ); + }, +}); diff --git a/packages/eve-browserbase/extension/tools/agent.ts b/packages/eve-browserbase/extension/tools/agent.ts new file mode 100644 index 0000000..2d5ceb6 --- /dev/null +++ b/packages/eve-browserbase/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 }, ctx) { + return withStagehand(ctx.session.id, async stagehand => { + const agent = stagehand.agent(); + return agent.execute({ + instruction, + maxSteps, + }); + }); + }, +}); diff --git a/packages/eve-browserbase/extension/tools/extract.ts b/packages/eve-browserbase/extension/tools/extract.ts new file mode 100644 index 0000000..a2b722f --- /dev/null +++ b/packages/eve-browserbase/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 }, ctx) { + const outputSchema = z.fromJSONSchema(schema); + return withStagehand(ctx.session.id, stagehand => + stagehand.extract(instruction, outputSchema) + ); + }, +}); diff --git a/packages/eve-browserbase/extension/tools/fetch.ts b/packages/eve-browserbase/extension/tools/fetch.ts new file mode 100644 index 0000000..1365edd --- /dev/null +++ b/packages/eve-browserbase/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/packages/eve-browserbase/extension/tools/navigate.ts b/packages/eve-browserbase/extension/tools/navigate.ts new file mode 100644 index 0000000..3f778d5 --- /dev/null +++ b/packages/eve-browserbase/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 }, 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/packages/eve-browserbase/extension/tools/observe.ts b/packages/eve-browserbase/extension/tools/observe.ts new file mode 100644 index 0000000..8365668 --- /dev/null +++ b/packages/eve-browserbase/extension/tools/observe.ts @@ -0,0 +1,20 @@ +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 }, ctx) { + return withStagehand(ctx.session.id, stagehand => + stagehand.observe(instruction) + ); + }, +}); diff --git a/packages/eve-browserbase/extension/tools/search.ts b/packages/eve-browserbase/extension/tools/search.ts new file mode 100644 index 0000000..6f7fa7b --- /dev/null +++ b/packages/eve-browserbase/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/packages/eve-browserbase/extension/tools/session.ts b/packages/eve-browserbase/extension/tools/session.ts new file mode 100644 index 0000000..f57d3d0 --- /dev/null +++ b/packages/eve-browserbase/extension/tools/session.ts @@ -0,0 +1,28 @@ +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(_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(_input, ctx) { + const session = await closeBrowserSession(ctx.session.id); + return { ...session, stopped: Boolean(session.id) }; + }, + }), + }), + }, +}); diff --git a/packages/eve-browserbase/package.json b/packages/eve-browserbase/package.json new file mode 100644 index 0000000..bba8e8d --- /dev/null +++ b/packages/eve-browserbase/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/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/packages/eve-browserbase/tsconfig.json b/packages/eve-browserbase/tsconfig.json new file mode 100644 index 0000000..ba1682b --- /dev/null +++ b/packages/eve-browserbase/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"] +}