agents
Version:
A home for your AI agents
265 lines (264 loc) • 10.7 kB
JavaScript
import { __DO_NOT_USE_WILL_BREAK__agentContext } from "../internal_context.js";
import { c as browserLinks, d as browserScrape, i as DurableBrowserSessionStore, l as browserMarkdown, o as browserContent, s as browserExtract, t as BrowserConnector } from "../connector-KEJnl6e5.js";
import { tool } from "ai";
import { z } from "zod";
import { DynamicWorkerExecutor, createCodemodeRuntime, truncateResult } from "@cloudflare/codemode";
//#region src/browser/ai.ts
let didWarnExperimental = false;
let didDebugQuickActionSkip = false;
/**
* The Durable Object state to build the runtime in: the explicit `ctx` if
* given, otherwise the current Agent's `ctx` (via `getCurrentAgent()`), so
* `createBrowserRuntime` can be called from an Agent method without threading
* `this.ctx` through.
*/
function resolveCtx(options) {
if (options.ctx) return options.ctx;
return (__DO_NOT_USE_WILL_BREAK__agentContext.getStore()?.agent)?.ctx;
}
function connectorOptions(options, ctx) {
if (options.cdpUrl) return {
cdpUrl: options.cdpUrl,
cdpHeaders: options.cdpHeaders,
timeout: options.timeout
};
if (!options.browser) throw new Error("Either 'browser' (Fetcher binding) or 'cdpUrl' must be provided");
return {
browser: options.browser,
store: options.store ?? new DurableBrowserSessionStore(ctx.storage),
session: options.session,
timeout: options.timeout
};
}
/**
* Create the browser codemode runtime: the `browser_execute` tool plus the
* runtime handle and connector for host-side wiring (approvals, session info,
* sweeps).
*
* @example
* ```ts
* export class MyAgent extends Agent<Env> {
* get browser() {
* return createBrowserRuntime({
* ctx: this.ctx,
* browser: this.env.BROWSER,
* loader: this.env.LOADER,
* session: { mode: "dynamic" }
* });
* }
*
* @callable()
* async closeBrowserSession() {
* await this.browser.connector.closeSession();
* }
* }
* ```
*/
function createBrowserRuntime(options) {
if (!didWarnExperimental) {
didWarnExperimental = true;
console.warn("[agents/browser] Browser tools are experimental and may change in a future release.");
}
const ctx = resolveCtx(options);
if (!ctx) throw new Error("createBrowserRuntime requires a Durable Object 'ctx' — pass it explicitly, or call from within an Agent so it can be resolved via getCurrentAgent()");
const connector = new BrowserConnector(ctx, connectorOptions(options, ctx));
const runtime = createCodemodeRuntime({
ctx,
executor: new DynamicWorkerExecutor({
loader: options.loader,
timeout: options.timeout
}),
connectors: [connector],
name: options.name ?? "browser",
transformResult: truncateResult
});
const tools = { browser_execute: runtime.tool() };
if (options.quickActions !== false) {
const qa = options.quickActions == null || options.quickActions === true ? {} : options.quickActions;
const quickActionBrowser = qa.browser ?? options.browser;
if (quickActionBrowser) Object.assign(tools, createQuickActionTools({
browser: quickActionBrowser,
actions: qa.actions,
maxChars: qa.maxChars,
options: qa.options
}));
else if (options.quickActions) throw new Error("quickActions requires a Browser Run binding — set 'browser' (env.BROWSER) or 'quickActions.browser'");
else if (!didDebugQuickActionSkip) {
didDebugQuickActionSkip = true;
console.debug("[agents/browser] Quick Action tools skipped — no Browser Run binding (only 'cdpUrl' is set). Pass 'browser' or 'quickActions.browser' to enable them.");
}
}
return {
runtime,
connector,
tools
};
}
/**
* Create AI SDK tools for browser automation via CDP code mode.
*
* Returns a `ToolSet` with a single durable `browser_execute` tool backed by
* a codemode runtime: the model writes TypeScript against the `cdp` connector
* (`cdp.send`, `cdp.attachToTarget`, `cdp.spec`, …), executions are recorded
* for abort-and-replay, and browser sessions survive pauses.
*
* @example
* ```ts
* import { createBrowserTools } from "agents/browser/ai";
* import { generateText } from "ai";
*
* // inside a Durable Object / Agent:
* const browserTools = createBrowserTools({
* ctx: this.ctx,
* browser: this.env.BROWSER,
* loader: this.env.LOADER,
* });
*
* const result = await generateText({
* model,
* tools: { ...browserTools, ...otherTools },
* messages,
* });
* ```
*/
function createBrowserTools(options) {
return createBrowserRuntime(options).tools;
}
const DEFAULT_QUICK_ACTION_TOOLS = [
"markdown",
"extract",
"links",
"scrape"
];
const DEFAULT_QUICK_ACTION_MAX_CHARS = 5e4;
const pageInputSchema = z.object({
url: z.string().url().optional().describe("URL of the page to load"),
html: z.string().optional().describe("Raw HTML to render instead of loading a URL")
}).refine((value) => Boolean(value.url) || Boolean(value.html), { message: "Provide either 'url' or 'html'" });
function toPage(input, options) {
const page = input.url ? { url: input.url } : { html: input.html };
return {
...options,
...page
};
}
function truncate(text, maxChars) {
if (maxChars <= 0 || text.length <= maxChars) return text;
return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters]`;
}
/**
* Keep a tool result within a rough character budget so a single browse cannot
* blow the model's context window — while preserving the result's shape so the
* model sees a consistent type across calls:
*
* - strings (markdown/content) are truncated to a string;
* - arrays (links/scrape) are trimmed from the end but stay arrays;
* - only an opaque oversized object (e.g. a sprawling `extract`) degrades to a
* `{ truncated, note, preview }` summary, since it cannot be trimmed safely.
*/
function boundResult(value, maxChars) {
if (maxChars <= 0) return value;
if (typeof value === "string") return truncate(value, maxChars);
let json;
try {
json = JSON.stringify(value);
} catch {
return value;
}
if (json.length <= maxChars) return value;
if (Array.isArray(value)) {
const trimmed = boundArray(value, maxChars);
if (trimmed.length > 0) return trimmed;
}
return {
truncated: true,
note: `Result is too large (${json.length} characters); narrow the request.`,
preview: `${json.slice(0, maxChars)}…`
};
}
/**
* Take as many leading items as fit within `maxChars` (measured against their
* JSON length), returning a trimmed array of the same element type. Silent by
* design: the model gets fewer, valid items rather than a reshaped result.
*/
function boundArray(value, maxChars) {
const out = [];
let size = 2;
for (const item of value) {
const itemSize = JSON.stringify(item).length + 1;
if (size + itemSize > maxChars) break;
out.push(item);
size += itemSize;
}
return out;
}
/**
* Create AI SDK tools for Browser Run [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/):
* stateless one-shot browsing (read a page as Markdown, extract structured
* data with AI, list links, scrape elements). Unlike `createBrowserTools`,
* these need only the `browser` binding — no Durable Object, loader, or
* sandbox — so they work from any Worker.
*
* @example
* ```ts
* import { createQuickActionTools } from "agents/browser/ai";
*
* const tools = createQuickActionTools({ browser: this.env.BROWSER });
* const result = await generateText({ model, tools, messages });
* ```
*/
function createQuickActionTools(options) {
const { browser } = options;
const requestOptions = options.options;
const enabled = new Set(options.actions ?? DEFAULT_QUICK_ACTION_TOOLS);
const maxChars = options.maxChars ?? DEFAULT_QUICK_ACTION_MAX_CHARS;
const tools = {};
if (enabled.has("markdown")) tools.browser_markdown = tool({
description: "Load a web page (or render raw HTML) and return its content as Markdown. Best for reading articles, docs, or any page as text.",
inputSchema: pageInputSchema,
execute: async (input) => boundResult(await browserMarkdown(browser, toPage(input, requestOptions)), maxChars)
});
if (enabled.has("extract")) tools.browser_extract = tool({
description: "Extract structured data from a web page using AI. Describe what you want in 'prompt'. Passing a JSON Schema in 'schema' is strongly recommended — without one the extractor often fails to produce JSON.",
inputSchema: z.object({
url: z.string().url().optional().describe("URL of the page to load"),
html: z.string().optional().describe("Raw HTML to render instead of loading a URL"),
prompt: z.string().optional().describe("What to extract, in natural language"),
schema: z.unknown().optional().describe("Optional JSON Schema describing the desired output")
}).refine((value) => Boolean(value.url) || Boolean(value.html), { message: "Provide either 'url' or 'html'" }).refine((value) => Boolean(value.prompt) || Boolean(value.schema), { message: "Provide either 'prompt' or 'schema'" }),
execute: async (input) => boundResult(await browserExtract(browser, {
...toPage(input, requestOptions),
prompt: input.prompt,
response_format: input.schema ? {
type: "json_schema",
schema: input.schema
} : void 0
}), maxChars)
});
if (enabled.has("links")) tools.browser_links = tool({
description: "Return every link found on a web page (including ones not visible). Useful for discovering pages to follow.",
inputSchema: pageInputSchema,
execute: async (input) => boundResult(await browserLinks(browser, toPage(input, requestOptions)), maxChars)
});
if (enabled.has("scrape")) tools.browser_scrape = tool({
description: "Scrape specific elements from a web page by CSS selector. Returns the matched elements' text, HTML, and attributes.",
inputSchema: z.object({
url: z.string().url().optional().describe("URL of the page to load"),
html: z.string().optional().describe("Raw HTML to render instead of loading a URL"),
selectors: z.array(z.string()).min(1).describe("CSS selectors to extract")
}).refine((value) => Boolean(value.url) || Boolean(value.html), { message: "Provide either 'url' or 'html'" }),
execute: async (input) => boundResult(await browserScrape(browser, {
...toPage(input, requestOptions),
elements: input.selectors.map((selector) => ({ selector }))
}), maxChars)
});
if (enabled.has("content")) tools.browser_content = tool({
description: "Load a web page and return its fully rendered HTML (after JavaScript runs). Prefer 'browser_markdown' unless you need the raw HTML.",
inputSchema: pageInputSchema,
execute: async (input) => boundResult(await browserContent(browser, toPage(input, requestOptions)), maxChars)
});
return tools;
}
//#endregion
export { createBrowserRuntime, createBrowserTools, createQuickActionTools };
//# sourceMappingURL=ai.js.map