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..7d1929c002 --- /dev/null +++ b/guides/browser-agent-in-sandbox/Dockerfile @@ -0,0 +1,10 @@ +# Prebuilt sandbox image for faster cold starts, built FROM the official `browse` +# CLI image 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 ghcr.io/browserbase/browse:latest +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..c7e7f8e66c --- /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 **system prompt is built fresh per request** (`buildSystemPrompt()`, not a static constant). It tells the agent it's running inside a sandbox microVM with no local browser (so it never passes `--local`), injects today's date at request time so it never guesses or hardcodes one, and tells it to give its best final answer from whatever it's gathered if it's nearing the step limit. + +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, point `BROWSE_VCR_IMAGE` at a prebuilt image — the `Dockerfile` here builds one from the official [`ghcr.io/browserbase/browse`](https://github.com/browserbase/stagehand/pkgs/container/browse) image: + +```dockerfile +FROM ghcr.io/browserbase/browse +WORKDIR /work +``` + +(Vercel Sandbox base images must live in VCR, so you build/mirror from the official image into your project's registry — the runtime doesn't pull `ghcr.io` directly.) + +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..b7da7c0336 --- /dev/null +++ b/guides/browser-agent-in-sandbox/ai/agent/constants.ts @@ -0,0 +1,19 @@ +// 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." + +// Built fresh on every request (not a static export) so the injected date is +// always today — the route calls this per-request, never at module load. +export function buildSystemPrompt(): string { + const today = new Date().toISOString().slice(0, 10) + return `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 skills show\` (it prints the CLI's bundled usage guide); use \`browse --help\` (and \`browse --help\`) for extra detail on any command. Then complete the task. + +You are running inside a Vercel Sandbox microVM with no local browser — every \`browse\` session is remote and already configured via environment variables, so never pass \`--local\`. Today's date is ${today}; use it for anything time-sensitive and never hardcode or guess a date. If you're nearing the step limit, stop navigating and give the best final answer you can from what you've already gathered. + +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..6c6008e459 --- /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, buildSystemPrompt } 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: buildSystemPrompt(), + 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() + }} + > +
+