UNPKG

agents

Version:

A home for your AI agents

265 lines (264 loc) 10.7 kB
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