Skip to content

Commit 49902a6

Browse files
authored
Update stagehand template to v4 (#221)
1 parent c64f4b5 commit 49902a6

6 files changed

Lines changed: 215 additions & 1819 deletions

File tree

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
# Copy this file to .env and fill in your API key
2-
OPENAI_API_KEY=your_openai_api_key_here
1+
# Copy this file to .env and set the model and its API key.
2+
# MODEL is provider-prefixed, e.g. anthropic/claude-sonnet-4-5, openai/gpt-4.1, google/gemini-2.5-flash
3+
MODEL=anthropic/claude-sonnet-4-5
4+
MODEL_API_KEY=your_api_key_here

‎pkg/templates/typescript/stagehand/README.md‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
# Kernel TypeScript Sample App - Stagehand
1+
# Kernel TypeScript Sample App - Stagehand (v4)
22

3-
A Stagehand-powered browser automation app that extracts team size information from Y Combinator company pages.
3+
A [Stagehand v4](https://docs.stagehand.dev) browser automation app that extracts team size information from Y Combinator company pages, running against a Kernel cloud browser.
44

55
## What it does
66

77
The `teamsize-task` searches for a startup on Y Combinator's company directory and extracts the team size (number of employees).
88

9+
## How Stagehand v4 connects to a Kernel browser
10+
11+
Stagehand v4 no longer drives the browser purely over CDP — it runs as a **Chrome extension** next to the browser. To use it with a remote Kernel browser, this template:
12+
13+
1. Creates a Kernel browser.
14+
2. Uploads the Stagehand extension (shipped inside the `@browserbasehq/stagehand` package) onto the running browser's filesystem, at the path Stagehand loads it from.
15+
3. Connects with `localBrowser.connect({ cdpUrl })` — with no `extensionId`, Stagehand loads the extension into the running browser over CDP (`Extensions.loadUnpacked`) — and `Stagehand.create({ browser })`.
16+
917
## Input
1018

1119
```json
@@ -24,12 +32,15 @@ The `teamsize-task` searches for a startup on Y Combinator's company directory a
2432

2533
## Setup
2634

27-
Create a `.env` file:
35+
Create a `.env` file with a provider-prefixed `MODEL` and its API key:
2836

2937
```
30-
OPENAI_API_KEY=your-openai-api-key
38+
MODEL=anthropic/claude-sonnet-4-5
39+
MODEL_API_KEY=your-api-key
3140
```
3241

42+
`MODEL` can be any provider Stagehand supports, e.g. `openai/gpt-4.1` or `google/gemini-2.5-flash`.
43+
3344
## Deploy
3445

3546
```bash
Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Stagehand } from "@browserbasehq/stagehand";
2-
import { Kernel, type KernelContext } from '@onkernel/sdk';
3-
import { z } from 'zod';
1+
import { Stagehand, localBrowser, type ModelName } from "@browserbasehq/stagehand";
2+
import { Kernel, type KernelContext } from "@onkernel/sdk";
3+
// Stagehand v4 uses zod v4 schema types.
4+
import { z } from "zod";
5+
import { loadStagehandExtension } from "./stagehand-extension";
46

57
const kernel = new Kernel();
68

7-
const app = kernel.app('ts-stagehand');
9+
const app = kernel.app("ts-stagehand");
810

911
interface CompanyInput {
1012
company: string;
@@ -14,17 +16,19 @@ interface TeamSizeOutput {
1416
teamSize: string;
1517
}
1618

17-
// LLM API Keys are set in the environment during `kernel deploy <filename> -e OPENAI_API_KEY=XXX`
19+
// Set MODEL (provider-prefixed, e.g. anthropic/claude-sonnet-4-5, openai/gpt-4.1,
20+
// google/gemini-2.5-flash) and MODEL_API_KEY for that provider in the environment
21+
// during `kernel deploy <filename> -e MODEL=XXX -e MODEL_API_KEY=YYY`.
1822
// See https://www.kernel.sh/docs/apps/deploy#environment-variables
23+
const MODEL = process.env.MODEL as ModelName | undefined;
24+
const MODEL_API_KEY = process.env.MODEL_API_KEY;
1925

20-
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
21-
22-
if (!OPENAI_API_KEY) {
23-
throw new Error('OPENAI_API_KEY is not set');
26+
if (!MODEL || !MODEL_API_KEY) {
27+
throw new Error("MODEL and MODEL_API_KEY must be set");
2428
}
2529

2630
app.action<CompanyInput, TeamSizeOutput>(
27-
'teamsize-task',
31+
"teamsize-task",
2832
async (ctx: KernelContext, payload?: CompanyInput): Promise<TeamSizeOutput> => {
2933
// A function that returns the team size of a Y Combinator startup
3034

@@ -35,7 +39,7 @@ app.action<CompanyInput, TeamSizeOutput>(
3539
// Returns:
3640
// output: The team size (number of employees) of the startup
3741

38-
const company = payload?.company || 'kernel';
42+
const company = payload?.company || "kernel";
3943

4044
const kernelBrowser = await kernel.browsers.create({
4145
invocation_id: ctx.invocation_id,
@@ -44,39 +48,51 @@ app.action<CompanyInput, TeamSizeOutput>(
4448

4549
console.log("Kernel browser live view url: ", kernelBrowser.browser_live_view_url);
4650

47-
const stagehand = new Stagehand({
48-
env: "LOCAL",
49-
localBrowserLaunchOptions: {
51+
// Stagehand only closes browsers it launched, so we close the connection and
52+
// delete the Kernel browser ourselves, even if the automation throws.
53+
let stagehand: Awaited<ReturnType<typeof Stagehand.create>> | undefined;
54+
let browser: Awaited<ReturnType<typeof localBrowser.connect>> | undefined;
55+
try {
56+
// Load the Stagehand extension into the running browser. With no
57+
// `extensionId`, `localBrowser.connect` then loads it over CDP.
58+
await loadStagehandExtension(kernel, kernelBrowser.session_id);
59+
browser = await localBrowser.connect({
5060
cdpUrl: kernelBrowser.cdp_ws_url,
51-
},
52-
model: "openai/gpt-4.1",
53-
apiKey: OPENAI_API_KEY,
54-
verbose: 1,
55-
domSettleTimeout: 30_000
56-
});
57-
await stagehand.init();
58-
59-
/////////////////////////////////////
60-
// Your Stagehand implementation here
61-
/////////////////////////////////////
62-
const page = stagehand.context.pages()[0];
63-
await page.goto("https://www.ycombinator.com/companies");
64-
65-
await stagehand.act(`Type in ${company} into the search box`);
66-
await stagehand.act("Click on the first search result");
67-
68-
// Schema definition
69-
const teamSizeSchema = z.object({
70-
teamSize: z.string(),
71-
});
72-
// Extract team size from the YC startup page
73-
const output = await stagehand.extract(
74-
"Extract the team size (number of employees) shown on this Y Combinator company page.",
75-
teamSizeSchema
76-
);
77-
await stagehand.close();
78-
await kernel.browsers.deleteByID(kernelBrowser.session_id);
79-
80-
return output;
61+
});
62+
stagehand = await Stagehand.create({
63+
browser,
64+
model: { modelName: MODEL, apiKey: MODEL_API_KEY },
65+
logging: { level: "info" },
66+
});
67+
68+
/////////////////////////////////////
69+
// Your Stagehand implementation here
70+
/////////////////////////////////////
71+
const page = await browser.context.activePage();
72+
if (!page) throw new Error("No active page in the Kernel browser");
73+
await page.goto("https://www.ycombinator.com/companies");
74+
75+
await stagehand.act(`Type in ${company} into the search box`);
76+
await stagehand.act("Click on the first search result");
77+
78+
// Extract team size from the YC startup page. Every v4 primitive returns { data }.
79+
const { data } = await stagehand.extract(
80+
"Extract the team size (number of employees) shown on this Y Combinator company page.",
81+
z.object({ teamSize: z.string() }),
82+
);
83+
84+
return data;
85+
} finally {
86+
// Nested so a rejected close() never skips deleting the Kernel browser.
87+
try {
88+
await stagehand?.close();
89+
} finally {
90+
try {
91+
await browser?.close();
92+
} finally {
93+
await kernel.browsers.deleteByID(kernelBrowser.session_id);
94+
}
95+
}
96+
}
8197
},
8298
);

‎pkg/templates/typescript/stagehand/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"private": true,
66
"dependencies": {
7-
"@browserbasehq/stagehand": "^3.5.0",
7+
"@browserbasehq/stagehand": "^4.0.0",
88
"@onkernel/sdk": "^0.23.0",
99
"zod": "^4.2.0"
1010
},

0 commit comments

Comments
 (0)