UNPKG

agents

Version:

A home for your AI agents

272 lines (271 loc) • 11.8 kB
import { c as browserExtract, f as browserScrape, i as DurableBrowserSessionStore, l as browserLinks, s as browserContent, t as BrowserConnector, u as browserMarkdown } from "../connector-Dl098To-.js"; import { i as transformBrowserResult, r as resolveCtx, t as browserExecuteModelOutput } from "../tool-helpers-F3_Wy2YX.js"; import { tool } from "ai"; import { z } from "zod"; import { DynamicWorkerExecutor, createCodemodeRuntime } from "@cloudflare/codemode"; //#region src/browser/ai.ts let didWarnExperimental = false; let didDebugQuickActionSkip = false; 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(); * } * } * ``` * * @deprecated Use `browserTool` from `agents/browser/ai-sdk`, * `agents/browser/tanstack-ai` or `agents/browser/pi`. See * https://github.com/cloudflare/agents/blob/main/docs/agents/browse-the-web.md#persistent-browser */ 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: transformBrowserResult }); const isKitesurf = !options.cdpUrl && options.session?.browser === "kitesurf"; const tools = { browser_execute: { ...runtime.tool({ connectorHints: { cdp: isKitesurf ? "Kitesurf one-shot Browser CDP. Call codemode.describe(\"cdp\") for connector types and Kitesurf execution rules. codemode.search indexes connector methods and snippets, not underlying CDP commands. Complete the task in one execution and do not pause. Return screenshots as { type: \"browser_screenshot\", mediaType: \"image/png\", data: screenshot.data }." : "Browser CDP. Return screenshots as { type: 'browser_screenshot', mediaType: 'image/png', data: screenshot.data }; the UI keeps the image while the model receives a compact summary." } }), toModelOutput: ({ output }) => browserExecuteModelOutput(output) } }; if (options.quickActions !== false && (!isKitesurf || options.quickActions !== void 0)) { const qa = typeof options.quickActions === "object" ? 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, * }); * ``` * * @deprecated Use `browserTool` from `agents/browser/ai-sdk`, * `agents/browser/tanstack-ai` or `agents/browser/pi`. See * https://github.com/cloudflare/agents/blob/main/docs/agents/browse-the-web.md#persistent-browser */ 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 }); * ``` * * @deprecated Quick Actions are deprecated and won't change. Use * `browserTool` to drive a browser, the `web_fetch` tool from * `agents/webfetch` to read a page, or call the binding's * `quickAction()` directly. */ 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.record(z.string(), z.unknown(), { error: "Schema must be a JSON object" }).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", json_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