diff --git a/browsers/file-io.mdx b/browsers/file-io.mdx index 9fbcd26..182f005 100644 --- a/browsers/file-io.mdx +++ b/browsers/file-io.mdx @@ -246,32 +246,34 @@ if __name__ == "__main__": -### Stagehand v3 +### Stagehand -When using Stagehand with Kernel browsers, you need to configure the download behavior in the `localBrowserLaunchOptions`: - -```typescript -const stagehand = new Stagehand({ - env: "LOCAL", - verbose: 1, - localBrowserLaunchOptions: { - cdpUrl: kernelBrowser.cdp_ws_url, - downloadsPath: DOWNLOAD_DIR, // Specify where downloads should be saved - acceptDownloads: true, // Enable downloads - }, -}); -``` +Stagehand v4 connects to the running Kernel browser (see the [Stagehand integration guide](/integrations/stagehand) for the full setup). A user-initiated download — e.g. clicking a download link — is saved to the browser's default download directory, `/home/kernel/Downloads`, which you retrieve with Kernel's File I/O APIs. No download-specific launch configuration is required. Here's a complete example: ```typescript -import { Stagehand } from "@browserbasehq/stagehand"; +import { Stagehand, localBrowser } from "@browserbasehq/stagehand"; import Kernel from "@onkernel/sdk"; import fs from "fs"; +import { createReadStream } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Kernel browsers save user-initiated downloads here by default. +const DOWNLOAD_DIR = "/home/kernel/Downloads"; + +// Stagehand v4 runs as a Chrome extension; mirror it onto the running browser +// so `localBrowser.connect` (no `extensionId`) can load it over CDP. +const stagehandDist = dirname(fileURLToPath(import.meta.resolve("@browserbasehq/stagehand"))); +async function loadStagehandExtension(kernel: Kernel, sessionId: string) { + await kernel.browsers.fs.uploadZip(sessionId, { + dest_path: join(stagehandDist, "extension"), + zip_file: createReadStream(join(stagehandDist, "assets/stagehand-extension.zip")), + }); +} -const DOWNLOAD_DIR = "/tmp/downloads"; - -// Poll listFiles until any file appears in the directory +// Poll listFiles until a completed file appears (skip in-progress .crdownload files). async function waitForFile( kernel: Kernel, sessionId: string, @@ -281,8 +283,9 @@ async function waitForFile( const start = Date.now(); while (Date.now() - start < timeoutMs) { const files = await kernel.browsers.fs.listFiles(sessionId, { path: dir }); - if (files.length > 0) { - return files[0]; + const done = files.find((f) => !f.name.endsWith(".crdownload")); + if (done) { + return done; } await new Promise((r) => setTimeout(r, 500)); } @@ -293,62 +296,68 @@ async function main() { const kernel = new Kernel(); console.log("Creating browser via Kernel..."); - const kernelBrowser = await kernel.browsers.create({ - stealth: true, - }); + const kernelBrowser = await kernel.browsers.create({ stealth: true }); console.log(`Kernel Browser Session Started`); console.log(`Session ID: ${kernelBrowser.session_id}`); console.log(`Watch live: ${kernelBrowser.browser_live_view_url}`); - // Initialize Stagehand with Kernel's CDP URL and download configuration - const stagehand = new Stagehand({ - env: "LOCAL", - verbose: 1, - localBrowserLaunchOptions: { - cdpUrl: kernelBrowser.cdp_ws_url, - downloadsPath: DOWNLOAD_DIR, - acceptDownloads: true, - }, - }); - - await stagehand.init(); - - const page = stagehand.context.pages()[0]; - - await page.goto("https://browser-tests-alpha.vercel.app/api/download-test"); - - // Use Stagehand to click the download button - await stagehand.act("Click the download file link"); - console.log("Download triggered"); - - // Wait for the file to be fully available via Kernel's File I/O APIs - console.log("Waiting for file to appear..."); - const downloadedFile = await waitForFile( - kernel, - kernelBrowser.session_id, - DOWNLOAD_DIR - ); - console.log(`File found: ${downloadedFile.name}`); - - const remotePath = `${DOWNLOAD_DIR}/${downloadedFile.name}`; - console.log(`Reading file from: ${remotePath}`); - - // Read the file from Kernel browser's filesystem - const resp = await kernel.browsers.fs.readFile(kernelBrowser.session_id, { - path: remotePath, - }); - - // Save to local filesystem - const bytes = await resp.bytes(); - fs.mkdirSync("downloads", { recursive: true }); - const localPath = `downloads/${downloadedFile.name}`; - fs.writeFileSync(localPath, bytes); - console.log(`Saved to ${localPath}`); - - // Clean up - await stagehand.close(); - await kernel.browsers.deleteByID(kernelBrowser.session_id); + let stagehand: Awaited> | undefined; + let browser: Awaited> | undefined; + try { + await loadStagehandExtension(kernel, kernelBrowser.session_id); + browser = await localBrowser.connect({ cdpUrl: kernelBrowser.cdp_ws_url }); + stagehand = await Stagehand.create({ + browser, + model: { + modelName: "anthropic/claude-sonnet-4-5", + apiKey: process.env.MODEL_API_KEY, + }, + }); + + const page = await browser.context.activePage(); + if (!page) throw new Error("No active page in the Kernel browser"); + await page.goto("https://browser-tests-alpha.vercel.app/api/download-test"); + + // Use Stagehand to click the download button + await stagehand.act("Click the download file link"); + console.log("Download triggered"); + + // Wait for the file to be fully available via Kernel's File I/O APIs + console.log("Waiting for file to appear..."); + const downloadedFile = await waitForFile( + kernel, + kernelBrowser.session_id, + DOWNLOAD_DIR + ); + console.log(`File found: ${downloadedFile.name}`); + + const remotePath = `${DOWNLOAD_DIR}/${downloadedFile.name}`; + console.log(`Reading file from: ${remotePath}`); + + // Read the file from the Kernel browser's filesystem + const resp = await kernel.browsers.fs.readFile(kernelBrowser.session_id, { + path: remotePath, + }); + + // Save to local filesystem + const bytes = await resp.bytes(); + fs.mkdirSync("downloads", { recursive: true }); + const localPath = `downloads/${downloadedFile.name}`; + fs.writeFileSync(localPath, bytes); + console.log(`Saved to ${localPath}`); + } finally { + // Nested so a rejected close() never skips deleting the Kernel browser. + try { + await stagehand?.close(); + } finally { + try { + await browser?.close(); + } finally { + await kernel.browsers.deleteByID(kernelBrowser.session_id); + } + } + } console.log("Browser session closed"); } diff --git a/integrations/stagehand.mdx b/integrations/stagehand.mdx index d633f02..d127f49 100644 --- a/integrations/stagehand.mdx +++ b/integrations/stagehand.mdx @@ -5,12 +5,43 @@ title: "Stagehand" [Stagehand](https://github.com/browserbase/stagehand) is an open source AI browser automation framework. It lets developers choose what to write in code vs. natural language. By integrating with Kernel, you can run Stagehand automations with cloud-hosted browsers. -This guide is compatible with Stagehand SDK v3. If you're using an earlier version, please refer to the [Stagehand migration guide](https://docs.stagehand.dev/v3/migrations/v2) or upgrade to v3. +This guide targets Stagehand SDK v4. Stagehand v4 runs as a Chrome extension alongside the browser rather than driving it purely over CDP, so a remote Kernel browser needs the extension loaded into it (covered below). If you're on an earlier version, see the [Stagehand migration guide](https://docs.stagehand.dev). -## Adding Kernel to existing Stagehand implementations +## Quick start with the Stagehand template -If you already have a Stagehand (v3) implementation, you can easily switch to using Kernel's cloud browsers by updating your browser configuration. +The fastest way to run Stagehand on Kernel is our app template, which comes pre-wired for v4: + +```bash +kernel create --name my-stagehand-app --language typescript --template stagehand +``` + +This scaffolds a self-contained app with two files: + +- `index.ts` — the automation (searches a startup on Y Combinator and extracts its team size). +- `stagehand-extension.ts` — a helper that loads the Stagehand extension onto the Kernel browser. + +Set a provider-prefixed `MODEL` and its API key in a `.env` file: + +```bash .env +# MODEL is provider-prefixed, e.g. anthropic/claude-sonnet-4-5, openai/gpt-4.1, google/gemini-2.5-flash +MODEL=anthropic/claude-sonnet-4-5 +MODEL_API_KEY=your-api-key +``` + +Then deploy and invoke: + +```bash +kernel deploy index.ts --env-file .env +kernel invoke ts-stagehand teamsize-task --payload '{"company": "kernel"}' +# → {"teamSize":"6"} +``` + +See the [deploy](/apps/deploy) and [invoke](/apps/invoke) guides for more. + +## Adding Kernel to an existing Stagehand v4 project + +If you already have a Stagehand v4 implementation, switch it to Kernel's cloud browsers with the steps below. ### 1. Install the Kernel SDK @@ -18,72 +49,95 @@ If you already have a Stagehand (v3) implementation, you can easily switch to us npm install @onkernel/sdk ``` -### 2. Initialize Kernel and create a browser +Stagehand v4 requires `zod` v4 for its schema types (it pins `zod@4.4.3`). If your project is on zod v3, upgrade it. -Import the libraries and create a cloud browser session: +### 2. Load the Stagehand extension onto the Kernel browser + +Stagehand v4 runs as a Chrome extension. When `localBrowser.connect` is called without an `extensionId`, Stagehand loads the extension into the running browser over CDP (`Extensions.loadUnpacked`), reading it from a path on the **browser's** filesystem. Mirror the extension — shipped inside the `@browserbasehq/stagehand` package — onto the running Kernel browser at that exact path first: ```typescript -import { Stagehand } from "@browserbasehq/stagehand"; -import Kernel from '@onkernel/sdk'; -import { z } from "zod"; +import { Kernel } from "@onkernel/sdk"; +import { createReadStream } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const stagehandDist = dirname(fileURLToPath(import.meta.resolve("@browserbasehq/stagehand"))); +const STAGEHAND_EXTENSION_ZIP = join(stagehandDist, "assets/stagehand-extension.zip"); +const STAGEHAND_EXTENSION_DIR = join(stagehandDist, "extension"); + +async function loadStagehandExtension(kernel: Kernel, sessionId: string): Promise { + await kernel.browsers.fs.uploadZip(sessionId, { + dest_path: STAGEHAND_EXTENSION_DIR, + zip_file: createReadStream(STAGEHAND_EXTENSION_ZIP), + }); +} +``` + +### 3. Create a browser and connect + +Create a Kernel browser, load the extension, then connect Stagehand to its CDP URL: + +```typescript +import { Stagehand, localBrowser } from "@browserbasehq/stagehand"; +import Kernel from "@onkernel/sdk"; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create({ stealth: true }); +console.log("Live view url:", kernelBrowser.browser_live_view_url); -console.log("Live view url: ", kernelBrowser.browser_live_view_url); -``` - -### 3. Update your browser configuration +await loadStagehandExtension(kernel, kernelBrowser.session_id); -Replace your existing browser setup to use Kernel's CDP URL: +// With no `extensionId`, Stagehand loads the extension over CDP. +const browser = await localBrowser.connect({ cdpUrl: kernelBrowser.cdp_ws_url }); -```typescript -const stagehand = new Stagehand({ - env: "LOCAL", - localBrowserLaunchOptions: { - cdpUrl: kernelBrowser.cdp_ws_url, +const stagehand = await Stagehand.create({ + browser, + model: { + modelName: "anthropic/claude-sonnet-4-5", + apiKey: process.env.MODEL_API_KEY, }, - model: "openai/gpt-4.1", - apiKey: process.env.OPENAI_API_KEY, - verbose: 1, - domSettleTimeout: 30_000 }); - -await stagehand.init(); ``` ### 4. Use your Stagehand automation -Use Stagehand's page methods with the Kernel-powered browser: +Drive the page with Stagehand's primitives. Note the v4 API: page access is async (`activePage()`), and `extract` returns its result under `data`: ```typescript -const page = stagehand.context.pages()[0]; -await page.goto("https://onkernel.com"); -await stagehand.act("Click on Blog in the navbar"); -await stagehand.act("Click on the newest blog post"); -const output = await stagehand.extract( - "Extract a summary of the blog post", - z.object({ summary: z.string() }) -); +import { z } from "zod"; + +const page = await browser.context.activePage(); +if (!page) throw new Error("No active page in the Kernel browser"); +await page.goto("https://www.ycombinator.com/companies"); -console.log("Newest blog post summary: ", output.summary); +await stagehand.act("Type in kernel into the search box"); +await stagehand.act("Click on the first search result"); + +const { data } = await stagehand.extract( + "Extract the team size (number of employees) shown on this Y Combinator company page.", + z.object({ teamSize: z.string() }), +); -// Clean up -await stagehand.close(); -await kernel.browsers.deleteByID(kernelBrowser.session_id); +console.log("Team size:", data.teamSize); ``` -## Quick setup with our Stagehand example app +### 5. Clean up -Alternatively, you can use our Kernel app template that includes a pre-configured Stagehand integration: +Stagehand v4 only closes browsers it launched, so close the connection and delete the Kernel browser yourself. Nest the cleanup so a failed `close()` never skips deleting the browser: -```bash -kernel create --name my-stagehand-app --language typescript --template stagehand +```typescript +try { + await stagehand.close(); +} finally { + try { + await browser.close(); + } finally { + await kernel.browsers.deleteByID(kernelBrowser.session_id); + } +} ``` -Then follow the [deploy](/apps/deploy) and [invoke](/apps/invoke) guides to deploy and run your Stagehand automation on Kernel's infrastructure. - ## Benefits of using Kernel with Stagehand - **No local browser management**: Run automations without installing or maintaining browsers locally diff --git a/reference/cli/create.mdx b/reference/cli/create.mdx index 32ffd15..bde2127 100644 --- a/reference/cli/create.mdx +++ b/reference/cli/create.mdx @@ -22,7 +22,7 @@ Create a new Kernel application from a template. The CLI provides an interactive - **`openai-computer-use`** — OpenAI Computer Using Agent (CUA) - **`gemini-computer-use`** — Google Gemini computer use agent - **`claude-agent-sdk`** — Claude Agent SDK browser automation agent -- **`stagehand`** — [Stagehand](https://github.com/browserbase/stagehand) v3 SDK integration +- **`stagehand`** — [Stagehand](https://github.com/browserbase/stagehand) v4 SDK integration - **`magnitude`** — [Magnitude](https://github.com/magnitude-labs/magnitude) SDK integration - **`tzafon`** — Tzafon Northstar CUA Fast computer use agent - **`yutori`** — Yutori n1.5 computer use agent