From 349ec6bf5837e10cea582ec59de043d9b463af67 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Tue, 30 Jun 2026 14:22:34 -0700 Subject: [PATCH 1/4] Add guides/browser-agent-in-sandbox: a browser-research agent in a Vercel Sandbox Signed-off-by: Shrey Pandya --- guides/browser-agent-in-sandbox/.env.example | 24 + guides/browser-agent-in-sandbox/.gitignore | 34 + guides/browser-agent-in-sandbox/.prettierrc | 4 + guides/browser-agent-in-sandbox/Dockerfile | 11 + guides/browser-agent-in-sandbox/README.md | 123 + .../ai/agent/constants.ts | 11 + .../browser-agent-in-sandbox/ai/agent/run.ts | 74 + .../ai/agent/sandbox.ts | 104 + .../app/api/chat/route.ts | 26 + .../browser-agent-in-sandbox/app/globals.css | 91 + .../browser-agent-in-sandbox/app/layout.tsx | 35 + guides/browser-agent-in-sandbox/app/page.tsx | 113 + .../browser-agent-in-sandbox/components.json | 21 + .../components/ai-elements/conversation.tsx | 62 + .../components/ai-elements/loader.tsx | 90 + .../components/chat/bash-call.tsx | 57 + .../components/chat/message.tsx | 60 + .../components/theme-provider.tsx | 11 + .../components/ui/button.tsx | 58 + .../components/ui/textarea.tsx | 18 + .../eslint.config.mjs | 14 + guides/browser-agent-in-sandbox/lib/types.ts | 20 + guides/browser-agent-in-sandbox/lib/utils.ts | 6 + .../browser-agent-in-sandbox/next.config.ts | 13 + guides/browser-agent-in-sandbox/package.json | 52 + .../browser-agent-in-sandbox/pnpm-lock.yaml | 7567 +++++++++++++++++ .../postcss.config.mjs | 5 + .../browser-agent-in-sandbox/scripts/smoke.ts | 58 + guides/browser-agent-in-sandbox/tsconfig.json | 33 + 29 files changed, 8795 insertions(+) create mode 100644 guides/browser-agent-in-sandbox/.env.example create mode 100644 guides/browser-agent-in-sandbox/.gitignore create mode 100644 guides/browser-agent-in-sandbox/.prettierrc create mode 100644 guides/browser-agent-in-sandbox/Dockerfile create mode 100644 guides/browser-agent-in-sandbox/README.md create mode 100644 guides/browser-agent-in-sandbox/ai/agent/constants.ts create mode 100644 guides/browser-agent-in-sandbox/ai/agent/run.ts create mode 100644 guides/browser-agent-in-sandbox/ai/agent/sandbox.ts create mode 100644 guides/browser-agent-in-sandbox/app/api/chat/route.ts create mode 100644 guides/browser-agent-in-sandbox/app/globals.css create mode 100644 guides/browser-agent-in-sandbox/app/layout.tsx create mode 100644 guides/browser-agent-in-sandbox/app/page.tsx create mode 100644 guides/browser-agent-in-sandbox/components.json create mode 100644 guides/browser-agent-in-sandbox/components/ai-elements/conversation.tsx create mode 100644 guides/browser-agent-in-sandbox/components/ai-elements/loader.tsx create mode 100644 guides/browser-agent-in-sandbox/components/chat/bash-call.tsx create mode 100644 guides/browser-agent-in-sandbox/components/chat/message.tsx create mode 100644 guides/browser-agent-in-sandbox/components/theme-provider.tsx create mode 100644 guides/browser-agent-in-sandbox/components/ui/button.tsx create mode 100644 guides/browser-agent-in-sandbox/components/ui/textarea.tsx create mode 100644 guides/browser-agent-in-sandbox/eslint.config.mjs create mode 100644 guides/browser-agent-in-sandbox/lib/types.ts create mode 100644 guides/browser-agent-in-sandbox/lib/utils.ts create mode 100644 guides/browser-agent-in-sandbox/next.config.ts create mode 100644 guides/browser-agent-in-sandbox/package.json create mode 100644 guides/browser-agent-in-sandbox/pnpm-lock.yaml create mode 100644 guides/browser-agent-in-sandbox/postcss.config.mjs create mode 100644 guides/browser-agent-in-sandbox/scripts/smoke.ts create mode 100644 guides/browser-agent-in-sandbox/tsconfig.json diff --git a/guides/browser-agent-in-sandbox/.env.example b/guides/browser-agent-in-sandbox/.env.example new file mode 100644 index 0000000000..6f00c2a45e --- /dev/null +++ b/guides/browser-agent-in-sandbox/.env.example @@ -0,0 +1,24 @@ +# --- Browserbase (the cloud browser the agent drives) --- +# Get a key at https://www.browserbase.com/settings +BROWSERBASE_API_KEY=bb_live_xxxxxxxxxxxxxxxxxxxxxxxx + +# --- Model auth: Vercel AI Gateway (no separate Anthropic key needed) --- +# The agent's model (anthropic/claude-sonnet-5) is served through Vercel AI +# Gateway, so the same Vercel auth that powers the Sandbox also powers the +# model. Pick ONE: +# +# 1. OIDC (recommended, keyless): run `vercel link` then `vercel env pull +# .env.local` in this directory. Vercel writes a short-lived +# VERCEL_OIDC_TOKEN to .env.local that both the Gateway and the Sandbox read +# automatically — no extra key to manage. (Refresh with `vercel env pull` +# when it expires.) When deployed to Vercel, OIDC is injected automatically. +# +# 2. AI Gateway API key: create one at https://vercel.com/dashboard/ai-gateway +# and set it here. Use this if you prefer a long-lived key over OIDC. +# AI_GATEWAY_API_KEY=vck_xxxxxxxxxxxxxxxxxxxxxxxx + +# --- Optional: faster cold start with a prebuilt sandbox image --- +# Set this to a Vercel Container Registry image that already has the `browse` +# CLI installed (see the "Template image" section of the README). When unset, +# the route falls back to a node24 runtime and installs `browse` per request. +# BROWSE_VCR_IMAGE=vcr.vercel.com///browse-cli:v1 diff --git a/guides/browser-agent-in-sandbox/.gitignore b/guides/browser-agent-in-sandbox/.gitignore new file mode 100644 index 0000000000..ab9293cce8 --- /dev/null +++ b/guides/browser-agent-in-sandbox/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +/node_modules +/.pnp +.pnp.js + +# Next.js +/.next/ +/out/ +next-env.d.ts + +# Production +build +dist + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local ENV files +.env +.env*.local + +# Vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/guides/browser-agent-in-sandbox/.prettierrc b/guides/browser-agent-in-sandbox/.prettierrc new file mode 100644 index 0000000000..b2095be81e --- /dev/null +++ b/guides/browser-agent-in-sandbox/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/guides/browser-agent-in-sandbox/Dockerfile b/guides/browser-agent-in-sandbox/Dockerfile new file mode 100644 index 0000000000..8cb32f3920 --- /dev/null +++ b/guides/browser-agent-in-sandbox/Dockerfile @@ -0,0 +1,11 @@ +# Prebuilt sandbox image for faster cold starts: the `browse` CLI is baked in, +# so the agent route can skip the per-request `npm install -g browse`. +# +# Build and push to your project's Vercel Container Registry, then set +# BROWSE_VCR_IMAGE to the pushed tag (see the README "Template image" section). +# +# Vercel Sandbox ignores ENTRYPOINT/CMD — the runtime drives the container — so +# we only need the CLI on PATH. +FROM node:24 +RUN npm i -g browse +WORKDIR /work diff --git a/guides/browser-agent-in-sandbox/README.md b/guides/browser-agent-in-sandbox/README.md new file mode 100644 index 0000000000..b788c99e31 --- /dev/null +++ b/guides/browser-agent-in-sandbox/README.md @@ -0,0 +1,123 @@ +# Browser Research Agent in a Vercel Sandbox + +A full-stack [Next.js](https://nextjs.org) app: an autonomous browser-research agent that drives the Browserbase [`browse`](https://www.npmjs.com/package/browse) CLI inside a [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) and streams its steps to the UI live. The agent plans its own steps — it runs `browse` commands through a `bash` tool to navigate the web and answer a research question. + +The `browse` CLI drives a real browser running on [Browserbase](https://www.browserbase.com), so the browser itself never runs in the sandbox — only the CLI does. That keeps the microVM cheap and the browser fully managed (stealth, CAPTCHA solving, proxies are all on the Browserbase side). + +## Demo + +[browser-agent-in-sandbox.vercel.app](https://browser-agent-in-sandbox.vercel.app) + +Enter a research task and watch the agent stream its `browse` steps live, then render the final comparison table. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/guides/browser-agent-in-sandbox&project-name=browser-agent-in-sandbox&repository-name=browser-agent-in-sandbox) + +## How it works + +``` +┌────────────────────────────┐ ┌────────────────────────────┐ CDP/wss ┌────────────────────────────┐ +│ Next.js app (your deploy) │ │ Vercel Sandbox (microVM) │ ─────────▶ │ Browserbase cloud browser │ +│ │ │ node24 (or VCR image) │ │ │ +│ /api/chat — agent loop │ ─bash─▶ │ `browse` CLI runs here │ ◀─────────── │ navigates / reads pages │ +│ (AI SDK streamText) │ │ │ page data │ │ +│ ▲ stream └────────────────────────────┘ └────────────────────────────┘ +│ │ +│ UI (useChat) renders each `browse` step + final answer +└────────────────────────────┘ +``` + +- The **agent loop runs in the `/api/chat` route**. It provisions a sandbox, builds a `bash` tool with [`bash-tool`](https://github.com/vercel-labs/bash-tool) bound to that sandbox, and runs the AI SDK's `streamText` with `stopWhen: stepCountIs(40)`. +- The **`bash` tool executes inside the sandbox**. The model navigates the web by emitting `browse` commands, which run in the microVM. +- The **model is served through [Vercel AI Gateway](https://vercel.com/docs/ai-gateway)**. A bare `provider/model` string (`anthropic/claude-sonnet-5`) resolves through the default Gateway provider in `ai@6`, authenticated by Vercel's OIDC token — so the same Vercel auth that powers the Sandbox also powers the model, and there's no separate `ANTHROPIC_API_KEY`. +- The **UI streams live**. `useChat` from `@ai-sdk/react` renders each `browse` tool call (with a compact running/done/error indicator) as it happens, and the final answer as Markdown (the comparison table). + +The default task is a genuinely browser-only example: on Amazon, search for the current top mechanical keyboards and, for the top 5 results, compare each product's title, price, star rating, and number of ratings — returning a comparison table with each product's URL. Amazon renders results client-side and returns nothing useful to a plain HTTP fetch, so the agent has to drive a real browser. + +## Setup + +The agent's model is served through AI Gateway, so **one Vercel auth covers both the Sandbox and the model — there is no separate Anthropic key.** + +You need: + +- `BROWSERBASE_API_KEY` — the cloud browser ([browserbase.com/settings](https://www.browserbase.com/settings)). It is passed to the sandbox as an encrypted environment variable, so it never lands in a file inside the sandbox. +- Model + Sandbox auth via Vercel — pick one: + - **OIDC (recommended, keyless):** link this directory to a Vercel project and pull its env. The `vercel` CLI writes a short-lived `VERCEL_OIDC_TOKEN` that both the Gateway and the Sandbox read automatically: + ```bash + vercel link # link to your Vercel project + vercel env pull .env.local # writes VERCEL_OIDC_TOKEN + ``` + When deployed to Vercel, OIDC is injected automatically — nothing to set. + - **AI Gateway API key:** create one at [vercel.com/dashboard/ai-gateway](https://vercel.com/dashboard/ai-gateway) and set `AI_GATEWAY_API_KEY`. (Sandbox auth still comes from OIDC / the Vercel deployment.) + +```bash +pnpm install +cp .env.example .env.local # add BROWSERBASE_API_KEY +vercel link # OIDC path: link the project… +vercel env pull .env.local # …and pull VERCEL_OIDC_TOKEN +pnpm dev # http://localhost:3000 +``` + +> AI Gateway requires the Vercel team to have billing enabled to service model requests (free credits unlock once a card is on file). This is the only model-side prerequisite — there is no Anthropic account or key. + +The default uses a plain `--remote` Browserbase session, which works on any plan. Verified browsers (residential IP + automatic CAPTCHA solving) are a Scale-plan upgrade — see [browserbase.com/pricing](https://www.browserbase.com/pricing). + +### Environment variables + +| Variable | Required | Description | +| --- | --- | --- | +| `BROWSERBASE_API_KEY` | yes | Authenticates `browse` against Browserbase. | +| `VERCEL_OIDC_TOKEN` | yes (keyless path) | Written by `vercel env pull`; powers both the Gateway and the Sandbox. Injected automatically on Vercel. | +| `AI_GATEWAY_API_KEY` | optional | Long-lived alternative to OIDC for the model only. | +| `BROWSE_VCR_IMAGE` | optional | Prebuilt sandbox image with `browse` baked in, for faster cold starts (see below). | + +## Template image (faster cold start) + +By default the route boots a `node24` sandbox and runs `npm install -g browse@latest` per request. To skip that install, bake `browse` into a sandbox image and point `BROWSE_VCR_IMAGE` at it. + +The `Dockerfile` in this directory builds that image: + +```dockerfile +FROM node:24 +RUN npm i -g browse +WORKDIR /work +``` + +Build and push it to your project's [Vercel Container Registry](https://vercel.com/docs/vercel-sandbox/images) (VCR), then set `BROWSE_VCR_IMAGE`: + +```bash +# Pull the project's OIDC token, then authenticate Docker to the Vercel +# Container Registry with it. +vercel env pull .env.local +printf '%s' "$VERCEL_OIDC_TOKEN" | docker login vcr.vercel.com --username oidc --password-stdin + +# Build for the sandbox platform (linux/amd64) with zstd compression and push +# straight to your project's registry. +docker buildx build --platform linux/amd64 \ + --output "type=image,name=vcr.vercel.com///browse-cli:v1,push=true,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true" \ + . + +# Then set the env var (locally in .env.local, or in your Vercel project): +# BROWSE_VCR_IMAGE=vcr.vercel.com///browse-cli:v1 +``` + +The image takes a moment to process after the push — `Sandbox.create` returns `image_not_ready` until it is Ready, after which the route boots from it. + +When `BROWSE_VCR_IMAGE` is set, the route creates the sandbox from that image and skips the install step. When it's unset, it falls back to the runtime install — both paths set `BROWSERBASE_API_KEY` and `BROWSE_SESSION=agent` in the sandbox env so `browse` runs remote and shares one session flag-free. + +## Project structure + +``` +ai/agent/ + constants.ts # model id, default task, system prompt + sandbox.ts # dual-mode sandbox creation + bash-tool adapter + run.ts # agent loop (streamText + bash tool) → UI message stream +app/ + api/chat/route.ts # POST endpoint useChat talks to + page.tsx # prompt box + live step stream + Markdown answer + layout.tsx # Geist fonts, theme +components/ # shadcn-style UI + chat rendering +scripts/smoke.ts # headless end-to-end smoke against real cloud +Dockerfile # prebuilt browse image for BROWSE_VCR_IMAGE +``` + +> Future direction: Vercel [`eve`](https://vercel.com/docs) (beta) is a purpose-built durable-agent framework to graduate to once it's GA — it would manage the agent's long-running lifecycle (provisioning, retries, durability) so the route wouldn't have to. diff --git a/guides/browser-agent-in-sandbox/ai/agent/constants.ts b/guides/browser-agent-in-sandbox/ai/agent/constants.ts new file mode 100644 index 0000000000..ad856d9b6a --- /dev/null +++ b/guides/browser-agent-in-sandbox/ai/agent/constants.ts @@ -0,0 +1,11 @@ +// The model is served through Vercel AI Gateway. A bare `provider/model` string +// resolves through the default Gateway provider in ai@6, authenticated by +// Vercel's OIDC token (or AI_GATEWAY_API_KEY) — so no separate Anthropic key is +// needed. +export const MODEL = 'anthropic/claude-sonnet-5' + +export const DEFAULT_TASK = + "Using Amazon (https://www.amazon.com), research the current top mechanical keyboards: search the site, then for the top 5 results compare each product's title, price, star rating, and number of ratings. Return a comparison table including each product's URL." + +export const SYSTEM_PROMPT = + 'You are an autonomous deep-research agent. You have a `browse` CLI (Browserbase browser automation) in your bash tool — it is installed, and its auth and a shared browser session are already configured via environment variables. Learn how to use it by running `browse --help` (and `browse --help` as needed), then complete the task. When you cite a document, link the direct document itself, not a viewer, preview, or index page that wraps it. Return a clear, well-sourced answer.' diff --git a/guides/browser-agent-in-sandbox/ai/agent/run.ts b/guides/browser-agent-in-sandbox/ai/agent/run.ts new file mode 100644 index 0000000000..c5f1e08172 --- /dev/null +++ b/guides/browser-agent-in-sandbox/ai/agent/run.ts @@ -0,0 +1,74 @@ +// Runs the browser-research agent and returns a UI message stream that +// `@ai-sdk/react`'s `useChat` consumes directly. +// +// The agent loop runs HERE on the host (Vercel AI SDK `streamText` with +// `stopWhen`). Its `bash` tool — built with `bash-tool` — executes commands +// INSIDE the Vercel Sandbox microVM. The model navigates the web by emitting +// `browse` commands through that `bash` tool. Tool calls + their results and the +// model's text stream to the client as standard AI SDK UI message parts. +import { + type ModelMessage, + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + stepCountIs, + streamText, +} from 'ai' +import { createBashTool } from 'bash-tool' +import { MODEL, SYSTEM_PROMPT } from './constants' +import { createBrowseSandbox, toBashToolSandbox } from './sandbox' +import type { ChatUIMessage } from '@/lib/types' + +interface RunAgentOptions { + /** Prior UI messages from `useChat` (includes the latest user turn). */ + messages?: ChatUIMessage[] + /** Or a bare prompt string (used by the smoke-test script). */ + prompt?: string +} + +export async function runBrowserAgent({ + messages, + prompt, +}: RunAgentOptions): Promise { + const sandbox = await createBrowseSandbox() + + // Build the `bash` tool backed by the sandbox. `destination` is the working + // directory the bash tool runs commands from. + const { tools } = await createBashTool({ + sandbox: toBashToolSandbox(sandbox), + destination: '/vercel/sandbox', + }) + + // Only expose the `bash` tool — that is all the agent needs to drive `browse`. + const agentTools = { bash: tools.bash } + + const modelMessages: ModelMessage[] = messages + ? await convertToModelMessages(messages) + : [{ role: 'user', content: prompt ?? '' }] + + return createUIMessageStreamResponse({ + stream: createUIMessageStream({ + originalMessages: messages, + execute: ({ writer }) => { + const result = streamText({ + model: MODEL, + system: SYSTEM_PROMPT, + messages: modelMessages, + tools: agentTools, + // The agent navigates the web over many steps; give it room. + stopWhen: stepCountIs(40), + onError: (error) => { + console.error('Agent error:', JSON.stringify(error, null, 2)) + }, + }) + + result.consumeStream() + writer.merge(result.toUIMessageStream({ sendStart: false })) + }, + onFinish: async () => { + // Tear down the microVM once the run completes. + await sandbox.stop().catch(() => {}) + }, + }), + }) +} diff --git a/guides/browser-agent-in-sandbox/ai/agent/sandbox.ts b/guides/browser-agent-in-sandbox/ai/agent/sandbox.ts new file mode 100644 index 0000000000..d66cd862c3 --- /dev/null +++ b/guides/browser-agent-in-sandbox/ai/agent/sandbox.ts @@ -0,0 +1,104 @@ +// Provisions a Vercel Sandbox (an ephemeral Firecracker microVM) with the +// Browserbase `browse` CLI available, and adapts it to `bash-tool`'s Sandbox +// interface so the agent's `bash` tool executes commands INSIDE the microVM. +// +// The `browse` CLI drives a real browser running on Browserbase, so the browser +// never runs in the microVM — only the CLI does. +import { Sandbox } from '@vercel/sandbox' +import type { Sandbox as BashToolSandbox } from 'bash-tool' + +// Default sandbox env vars. Vercel stores them encrypted server-side and injects +// them into every command, so they reach the `browse` commands the bash tool +// runs — without ever being written to a committed file or echoed back. +// BROWSERBASE_API_KEY — authenticates `browse` against Browserbase. +// BROWSE_SESSION=agent — steers `browse` to run remotely and share one named +// browser session across commands, so the agent never has to pass +// --remote/--session flags itself. +function sandboxEnv(): Record { + const browserbaseApiKey = process.env.BROWSERBASE_API_KEY + if (!browserbaseApiKey) { + throw new Error('Missing required env var: BROWSERBASE_API_KEY') + } + return { BROWSERBASE_API_KEY: browserbaseApiKey, BROWSE_SESSION: 'agent' } +} + +/** + * Create a sandbox with `browse` available. + * + * Dual-mode: + * - If `BROWSE_VCR_IMAGE` is set, boot from that prebuilt image (the `browse` + * CLI is baked in — see the README "Template image" section). This skips the + * per-request npm install for a faster cold start. + * - Otherwise, boot the `node24` runtime and `npm install -g browse@latest`. + */ +export async function createBrowseSandbox(): Promise { + const env = sandboxEnv() + const image = process.env.BROWSE_VCR_IMAGE + + if (image) { + return Sandbox.create({ image, env, timeout: 600_000 }) + } + + const sandbox = await Sandbox.create({ + runtime: 'node24', + env, + timeout: 600_000, + }) + + const install = await sandbox.runCommand('npm', [ + 'install', + '-g', + 'browse@latest', + ]) + if (install.exitCode !== 0) { + await sandbox.stop() + throw new Error(`browse install failed (exit ${install.exitCode})`) + } + + return sandbox +} + +async function streamToString( + stream: NodeJS.ReadableStream +): Promise { + const chunks: Buffer[] = [] + for await (const chunk of stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + return Buffer.concat(chunks).toString('utf-8') +} + +/** + * Adapt a `@vercel/sandbox` v2 instance to `bash-tool`'s Sandbox interface + * (executeCommand / readFile / writeFiles). `bash-tool` ships a built-in + * wrapper, but its duck-typing detection doesn't recognize `@vercel/sandbox` v2, + * so we pass this small explicit adapter (documented in the bash-tool README). + * `bash` executes IN the microVM; the agent loop stays on the host. + */ +export function toBashToolSandbox(sandbox: Sandbox): BashToolSandbox { + return { + async executeCommand(command: string) { + const result = await sandbox.runCommand('bash', ['-c', command]) + const [stdout, stderr] = await Promise.all([ + result.stdout(), + result.stderr(), + ]) + return { stdout, stderr, exitCode: result.exitCode } + }, + async readFile(filePath: string) { + const stream = await sandbox.readFile({ path: filePath }) + if (stream === null) throw new Error(`File not found: ${filePath}`) + return streamToString(stream) + }, + async writeFiles(files) { + await sandbox.writeFiles( + files.map((f) => ({ + path: f.path, + content: Buffer.isBuffer(f.content) + ? f.content + : Buffer.from(f.content), + })) + ) + }, + } +} diff --git a/guides/browser-agent-in-sandbox/app/api/chat/route.ts b/guides/browser-agent-in-sandbox/app/api/chat/route.ts new file mode 100644 index 0000000000..8fad42de8b --- /dev/null +++ b/guides/browser-agent-in-sandbox/app/api/chat/route.ts @@ -0,0 +1,26 @@ +import { runBrowserAgent } from '@/ai/agent/run' +import { DEFAULT_TASK } from '@/ai/agent/constants' +import type { ChatUIMessage } from '@/lib/types' + +// The agent loop provisions a sandbox and runs many browser steps — keep it on +// the Node.js runtime and give it a long budget. On Vercel this needs Fluid +// Compute enabled for the streaming + long duration to work. +export const runtime = 'nodejs' +export const maxDuration = 800 + +interface BodyData { + messages?: ChatUIMessage[] + prompt?: string +} + +export async function POST(req: Request) { + const { messages, prompt } = (await req.json()) as BodyData + + // `useChat` sends `messages`. The smoke-test script can send a bare `prompt`. + // If neither is present, fall back to the default research task. + if (messages && messages.length > 0) { + return runBrowserAgent({ messages }) + } + + return runBrowserAgent({ prompt: prompt ?? DEFAULT_TASK }) +} diff --git a/guides/browser-agent-in-sandbox/app/globals.css b/guides/browser-agent-in-sandbox/app/globals.css new file mode 100644 index 0000000000..1143237d96 --- /dev/null +++ b/guides/browser-agent-in-sandbox/app/globals.css @@ -0,0 +1,91 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; +@source "../node_modules/streamdown/dist/*.js"; + +@custom-variant dark (&:where(.dark, .dark *)); + +:root { + --background: rgb(255, 255, 255); + --foreground: rgb(10, 10, 10); + --card: rgb(255, 255, 255); + --card-foreground: rgb(10, 10, 10); + --popover: rgb(255, 255, 255); + --popover-foreground: rgb(10, 10, 10); + --primary: rgb(23, 23, 23); + --primary-foreground: rgb(250, 250, 250); + --secondary: rgb(250, 250, 250); + --secondary-foreground: rgb(64, 64, 64); + --muted: rgb(245, 245, 245); + --muted-foreground: rgb(113, 113, 113); + --accent: rgb(245, 245, 245); + --accent-foreground: rgb(23, 23, 23); + --destructive: rgb(220, 38, 38); + --destructive-foreground: rgb(250, 250, 250); + --border: rgb(229, 229, 229); + --input: rgb(229, 229, 229); + --ring: rgb(161, 161, 161); + --radius: 0.625rem; +} + +.dark { + --background: rgb(10, 10, 10); + --foreground: rgb(250, 250, 250); + --card: rgb(23, 23, 23); + --card-foreground: rgb(250, 250, 250); + --popover: rgb(38, 38, 38); + --popover-foreground: rgb(250, 250, 250); + --primary: rgb(245, 245, 245); + --primary-foreground: rgb(23, 23, 23); + --secondary: rgb(38, 38, 38); + --secondary-foreground: rgb(229, 229, 229); + --muted: rgb(38, 38, 38); + --muted-foreground: rgb(161, 161, 161); + --accent: rgb(38, 38, 38); + --accent-foreground: rgb(250, 250, 250); + --destructive: rgb(248, 113, 113); + --destructive-foreground: rgb(23, 23, 23); + --border: rgb(56, 56, 56); + --input: rgb(56, 56, 56); + --ring: rgb(115, 115, 115); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + /* Geist fonts are wired into these vars from app/layout.tsx. */ + --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-mono: var(--font-geist-mono), ui-monospace, monospace; + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + font-family: var(--font-sans); + } +} diff --git a/guides/browser-agent-in-sandbox/app/layout.tsx b/guides/browser-agent-in-sandbox/app/layout.tsx new file mode 100644 index 0000000000..56911c9f6d --- /dev/null +++ b/guides/browser-agent-in-sandbox/app/layout.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from 'next' +import type { ReactNode } from 'react' +import { GeistSans } from 'geist/font/sans' +import { GeistMono } from 'geist/font/mono' +import { ThemeProvider } from '@/components/theme-provider' +import './globals.css' + +export const metadata: Metadata = { + title: 'Browser Research Agent', + description: + 'A browser-research agent that drives the Browserbase browse CLI inside a Vercel Sandbox and streams its steps live.', +} + +export default function RootLayout({ + children, +}: Readonly<{ children: ReactNode }>) { + return ( + + + + {children} + + + + ) +} diff --git a/guides/browser-agent-in-sandbox/app/page.tsx b/guides/browser-agent-in-sandbox/app/page.tsx new file mode 100644 index 0000000000..45b1089888 --- /dev/null +++ b/guides/browser-agent-in-sandbox/app/page.tsx @@ -0,0 +1,113 @@ +'use client' + +import { useChat } from '@ai-sdk/react' +import { DefaultChatTransport } from 'ai' +import { useState } from 'react' +import { ArrowUpIcon, GlobeIcon, SquareIcon } from 'lucide-react' +import type { ChatUIMessage } from '@/lib/types' +import { DEFAULT_TASK } from '@/ai/agent/constants' +import { + Conversation, + ConversationContent, + ConversationScrollButton, +} from '@/components/ai-elements/conversation' +import { Loader } from '@/components/ai-elements/loader' +import { Message } from '@/components/chat/message' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' + +export default function Page() { + const [input, setInput] = useState(DEFAULT_TASK) + const { messages, sendMessage, status, stop } = useChat({ + transport: new DefaultChatTransport({ api: '/api/chat' }), + }) + + const busy = status === 'submitted' || status === 'streaming' + + function submit() { + const text = input.trim() + if (!text || busy) return + sendMessage({ text }) + } + + return ( +
+
+ +

+ Browser Research Agent +

+ + browse CLI · Vercel Sandbox · Browserbase + +
+ + {messages.length === 0 ? ( +
+

+ An autonomous agent that drives the Browserbase{' '} + browse CLI inside a Vercel + Sandbox. Enter a research task and watch it browse the web, step by + step. +

+
+ ) : ( + + + {messages.map((message) => ( + + ))} + {status === 'submitted' && ( +
+ + Provisioning sandbox and starting the agent… +
+ )} +
+ +
+ )} + +
{ + event.preventDefault() + submit() + }} + > +
+