diff --git a/packages/sdk/examples/browser/README.md b/packages/sdk/examples/browser/README.md index 9703b638..631a0ca7 100644 --- a/packages/sdk/examples/browser/README.md +++ b/packages/sdk/examples/browser/README.md @@ -13,8 +13,21 @@ node retrieval/01-catalog-extraction.ts - All examples need `UPSTASH_BOX_API_KEY`. - Examples marked **AI** below use metered browser AI (`act`, `extract`, `observe`) and need a model provider key configured on the box or account. +- `agentic/01-in-box-agent.ts` is different: it drives the browser through the + box's coding agent, so it needs an agent harness + key and bills coding-agent + tokens, not browser-AI metering. - Everything else runs with the Box key alone. +## agentic/ — goal-driven browsing (replacing the removed agent loop) + +Stagehand v4 removed the built-in agent loop. Two replacements, matching v4's +guidance: + +| File | AI | What it shows | +| --- | --- | --- | +| `01-in-box-agent.ts` | coding agent | Hand a goal to the box's in-sandbox agent; `browser: true` wires the chrome-devtools MCP, so it drives the browser itself and writes results to the box. Needs an agent harness + key | +| `02-build-your-own-loop.ts` | yes | A live `observe` then `act(action)` then `extract` loop you own, bounded by a step budget with `extract` as the stop check. Model in the loop | + ## automation/ — forms, files, and durable sessions | File | AI | What it shows | diff --git a/packages/sdk/examples/browser/agentic/01-in-box-agent.ts b/packages/sdk/examples/browser/agentic/01-in-box-agent.ts new file mode 100644 index 00000000..7af98d8d --- /dev/null +++ b/packages/sdk/examples/browser/agentic/01-in-box-agent.ts @@ -0,0 +1,51 @@ +import { Box, Agent } from "@upstash/box"; + +// Agentic browsing, in-box agent. +// Stagehand v4 removed the built-in agent loop. On Box the replacement is not a +// client-side loop: enabling the browser (`browser: true`) auto-wires the +// chrome-devtools MCP (chrome-devtools-mcp on 127.0.0.1:9222, Chromium already +// warmed) into the box's coding agent. So `box.agent.run` hands the goal to an +// agent that lives IN the sandbox — it writes and runs the browsing work itself +// and iterates until done. Same job as Stagehand v4 "code mode", different +// primitive (an in-box coding agent + MCP, not an SDK call you script). +// +// Cost note: this bills the coding agent's model tokens, not browser-AI metering +// (`act`/`extract`/`observe`). It needs an agent harness + key, not just the box. + +const box = await Box.create({ + apiKey: process.env.UPSTASH_BOX_API_KEY!, + runtime: "node", + browser: true, + agent: { + harness: Agent.ClaudeCode, + model: "anthropic/claude-sonnet-4-5", + apiKey: process.env.CLAUDE_KEY!, + }, +}); + +try { + // No tab.create first: Chromium is warmed for browser boxes, and the agent + // drives it through the chrome-devtools MCP. + const run = await box.agent.run({ + prompt: [ + "Use the browser to scrape https://books.toscrape.com/.", + "Open the catalogue, visit the first 5 product pages, and for each one record", + "{ title, price, stock }.", + "Write the array as JSON to books.json in the working directory.", + "Verify the file parses and has 5 entries before you finish.", + ].join(" "), + }); + + console.log("run:", run.status, run.cost); + console.log(run.result); // the agent's final message + + if (run.status !== "completed") { + console.error("agent run did not complete; not reading books.json"); + } else { + // The agent produced the file inside the box; read + parse it back out. + const books = JSON.parse(await box.files.read("books.json")); + console.log(books); + } +} finally { + await box.delete(); +} diff --git a/packages/sdk/examples/browser/agentic/02-build-your-own-loop.ts b/packages/sdk/examples/browser/agentic/02-build-your-own-loop.ts new file mode 100644 index 00000000..989b732c --- /dev/null +++ b/packages/sdk/examples/browser/agentic/02-build-your-own-loop.ts @@ -0,0 +1,58 @@ +import { writeFile } from "node:fs/promises"; +import { Box } from "@upstash/box"; +import { z } from "zod/v3"; + +// Agentic browsing, your own loop. +// Stagehand v4's other replacement for the removed agent loop: keep the model in +// the loop and own the control flow. Unlike 02-ai-compiled-scraper (which reads +// the layout ONCE, then scrapes deterministically with Playwright and no more +// AI), the model still runs here on every page. Resolve the product links once +// with observe, then for each: reset to the listing (deterministic goto, no +// browser-AI tokens), replay the resolved click with no LLM (act(action)), and +// extract the product. extract is the loop's stop check. + +const START = "https://books.toscrape.com/"; +const GOAL = 5; + +const box = await Box.create({ + apiKey: process.env.UPSTASH_BOX_API_KEY!, + runtime: "node", + browser: true, +}); + +try { + const tab = await box.browser.tab.create(START, { waitUntil: "domcontentloaded" }); + + // Resolve the product links ONCE (metered), keeping those with a selector so + // act() can replay each click. The listing exposes two links per book (cover + + // title), so we dedupe by title and stop once we have GOAL distinct books. + const { elements } = await tab.observe("the book product links in the listing"); + const actions = elements.filter((e) => e.selector); + + const books: Array<{ title: string; price: string; stock: string }> = []; + const seen = new Set(); + for (const action of actions) { + if (books.length >= GOAL) break; // stop once we have enough distinct books + + await tab.goto(START); // deterministic reset, no browser-AI tokens + await tab.act(action); // replay the resolved click: no LLM, no tokens + + const book = await tab.extract( + "the book title, price, and availability text on this product page", + z.object({ + title: z.string().nullable(), + price: z.string().nullable(), + stock: z.string().nullable(), + }), + ); + if (!book.title || !book.price) continue; // not a product page + if (seen.has(book.title)) continue; // skip the duplicate cover/title link + seen.add(book.title); + books.push({ title: book.title, price: book.price, stock: book.stock ?? "" }); + } + + await writeFile("books.json", JSON.stringify(books, null, 2)); + console.log(`collected ${books.length}/${GOAL} books`); +} finally { + await box.delete(); +}