UNPKG

agents

Version:

A home for your AI agents

360 lines (358 loc) • 12.1 kB
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js"; import { n as BrowserSession, o as browserFunctionDocs } from "./session-CRY0bGiI.js"; import { a as truncateResult, i as transformBrowserResult } from "./tool-helpers-F3_Wy2YX.js"; import { RpcTarget } from "cloudflare:workers"; //#region src/browser/sandbox.ts /** * Runs model-written JavaScript in a Dynamic Worker, with a `browser` * object whose functions run on the host. Used by `browserTool`. * Internal — not an entry point. * * The Worker has no network access (`globalOutbound: null`) and no bindings: * the only way out is `browser`, whose calls go back to the host over RPC. */ /** * Code run as a function body returns nothing unless it says `return`. Tell * the model, rather than parse the code to return its last expression. A text * search for `return` can't tell a real one from one inside a string, so any * body run that comes back empty gets the notice. */ const STATEMENTS_NOTICE = "Your code ran as statements and returned nothing. If you wanted a value, `return` it, or write an async arrow function."; var _functions = /* @__PURE__ */ new WeakMap(); /** * The host side of `browser`. A failed call resolves with `{ error }` * instead of rejecting across RPC; the sandbox rethrows it as a local error * the code can catch. */ var HostFunctions = class extends RpcTarget { constructor(functions) { super(); _classPrivateFieldInitSpec(this, _functions, void 0); _classPrivateFieldSet2(_functions, this, functions); } async call(name, input) { const fn = Object.hasOwn(_classPrivateFieldGet2(_functions, this), name) ? _classPrivateFieldGet2(_functions, this)[name] : void 0; if (!fn) return { error: `browser.${name} is not a function.` }; try { return { value: await fn(input) }; } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } } }; /** Console lines a run keeps, and the characters kept of each. */ const MAX_LOG_LINES = 100; const MAX_LOG_LINE_CHARS = 2e3; /** * The entry module. `code.js` holds the model's code; keeping it in its own * module keeps the harness's names out of its scope. */ function mainModule(timeoutMs, names) { return `import { WorkerEntrypoint } from "cloudflare:workers"; import run from "./code.js"; export default class BrowserCode extends WorkerEntrypoint { async evaluate(binding) { // Logs are kept in the tool's stored output, so bound them here. const lines = []; let dropped = 0; const record = (prefix) => (...args) => { if (lines.length >= ${MAX_LOG_LINES}) { dropped++; return; } const line = prefix + args.map(String).join(" "); lines.push( line.length > ${MAX_LOG_LINE_CHARS} ? line.slice(0, ${MAX_LOG_LINE_CHARS}) + "…" : line ); }; const logs = () => dropped > 0 ? [...lines, "[" + dropped + " more lines dropped]"] : lines; console.log = record(""); console.info = record(""); console.warn = record("[warn] "); console.error = record("[error] "); const call = async (method, input) => { const reply = await binding.call(method, input); if ("error" in reply) throw new Error(reply.error); return reply.value; }; const browser = Object.freeze({ ${names.map((name) => ` ${JSON.stringify(name)}: (input) => call(${JSON.stringify(name)}, input),`).join("\n")} }); let timer; const timeout = new Promise((_, reject) => { timer = setTimeout( () => reject(new Error("Execution timed out after ${timeoutMs}ms")), ${timeoutMs} ); }); try { return { result: await Promise.race([run(browser), timeout]), logs: logs() }; } catch (error) { return { error: error instanceof Error ? error.message : String(error), logs: logs() }; } finally { clearTimeout(timer); } } } `; } /** Strip the markdown fences models often wrap code in. */ function stripFences(code) { return code.trim().match(/^```(?:js|javascript|typescript|ts|tsx|jsx)?\s*\n([\s\S]*?)```\s*$/)?.[1] ?? code.trim(); } /** * The model's code as an expression: an async arrow function (what the tool * asks for) is called, and any other expression is awaited. */ function expressionModule(code) { return `export default async function (browser) { const value = ( ${code.replace(/^export\s+default\s+/, "").replace(/;\s*(\/\/[^\n"'`]*)?\s*$/, "")} ); return typeof value === "function" ? await value() : await value; } `; } /** The model's code as a function body, for statements with a `return`. */ function bodyModule(code) { return `export default async function (browser) { ${code} } `; } /** The Worker failed to start because a module didn't parse. */ function isSyntaxError(error) { return error instanceof Error && /SyntaxError/.test(error.message); } /** * Run the model's code in a fresh Dynamic Worker, with `browser` bound to * `functions`. * * The code is first loaded as an expression (an async arrow function, as * the tool asks for). If that doesn't parse, it's loaded once more as a * function body, so statements ending in `return` work too; statements * with no `return` at all get a `notice`. Errors thrown * by the code, including timeouts, come back as `{ error }`; this only * rejects when the Worker can't run at all. */ async function runBrowserCode(options) { const code = stripFences(options.code); if (code === "") return { result: void 0, logs: [] }; const binding = new HostFunctions(options.functions); try { return await evaluate(options, expressionModule(code), binding); } catch (error) { if (!isSyntaxError(error)) throw error; } let ran; try { ran = await evaluate(options, bodyModule(code), binding); } catch (error) { if (!isSyntaxError(error)) throw error; return { error: `Your code didn't parse: ${error.message}. Write an async arrow function, async () => { ... }.`, logs: [] }; } return "result" in ran && ran.result === void 0 ? { ...ran, notice: STATEMENTS_NOTICE } : ran; } /** * How long past `timeoutMs` the host waits before giving up on a run whose * sandbox didn't time itself out. Capped at `timeoutMs` for short runs. */ const HOST_TIMEOUT_GRACE_MS = 5e3; async function evaluate(options, codeModule, binding) { const worker = options.loader.load({ compatibilityDate: "2025-06-01", mainModule: "main.js", modules: { "main.js": mainModule(options.timeoutMs, Object.keys(options.functions)), "code.js": codeModule }, globalOutbound: null }); try { const entrypoint = worker.getEntrypoint(); const running = entrypoint.evaluate(binding); running.catch(() => {}); let timer; const deadline = new Promise((resolve) => { timer = setTimeout(() => resolve({ error: `Execution timed out after ${options.timeoutMs}ms`, logs: [] }), options.timeoutMs + Math.min(HOST_TIMEOUT_GRACE_MS, options.timeoutMs)); }); const { signal } = options; let onAbort; const stopped = new Promise((resolve) => { onAbort = () => resolve({ error: "The run was stopped before it finished.", logs: [] }); if (signal?.aborted) onAbort(); else signal?.addEventListener("abort", onAbort, { once: true }); }); try { return await Promise.race([ running, deadline, stopped ]); } finally { if (onAbort) signal?.removeEventListener("abort", onAbort); clearTimeout(timer); disposeQuietly(entrypoint); } } finally { disposeQuietly(worker); } } function disposeQuietly(resource) { if (typeof resource !== "object" || resource === null || !(Symbol.dispose in resource)) return; const dispose = resource[Symbol.dispose]; if (typeof dispose !== "function") return; try { dispose.call(resource); } catch {} } //#endregion //#region src/browser/browser-tool.ts /** * The harness-neutral core of `browserTool`, shared by the adapters * in `tools/` (`agents/browser/ai-sdk`, `agents/browser/tanstack-ai`, * `agents/browser/pi`). Internal — not an entry point. */ const DEFAULT_TIMEOUT_MS = 6e4; /** * The model's input schema, as a Standard Schema with a JSON Schema view, * so the AI SDK and TanStack AI both accept it without zod. */ const inputJsonSchema = { type: "object", properties: { code: { type: "string", description: "An async arrow function that drives the browser with `browser`" } }, required: ["code"], additionalProperties: false }; const browserToolInputSchema = { "~standard": { version: 1, vendor: "agents", types: void 0, validate: (value) => typeof value === "object" && value !== null && "code" in value && typeof value.code === "string" ? { value: { code: value.code } } : { issues: [{ message: "Expected an object with a string code property" }] }, jsonSchema: { input: (_options) => inputJsonSchema, output: (_options) => inputJsonSchema } } }; /** * Build the description and `execute` for one `browserTool`. * * Each run opens a session on the browser in its scope, runs the model's * code in a fresh Dynamic Worker with `browser` bound to that session, closes * the session, and adds its report's notices to the result. Runs in one * scope lock it, since they drive one tab, so they wait for each other and * for other locking sessions; runs in different scopes go in parallel. */ function createBrowserToolCore(options, adapter) { const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; const docs = browserFunctionDocs(options.browser.internalHosts); const rules = [ ...docs.rules, "Results over about 24,000 characters are cut short.", `Each run times out after ${Math.round(timeoutMs / 1e3)}s, so split long jobs (crawling a site, say) across runs.`, adapter.screenshotHint, "If your code doesn't parse, the line number in the error counts from wrapper code, not yours. Check regex literals and escapes." ].map((rule) => `- ${rule}`).join("\n"); const description = [ "Run JavaScript that drives a persistent Chrome browser over the Chrome DevTools Protocol (CDP).", "", "Write an async arrow function, `async () => { ... }`, and return a small JSON value. Use the `browser` global to reach the browser. `fetch` is blocked and there is no file system or Node.js API.", "", "## browser", "", docs.functions, "", "## Rules", "", rules ].join("\n"); const defaultScope = options.scope ?? "shared"; async function run(input, scope, signal) { const session = new BrowserSession(options.browser, { scope, lock: true }); try { await session.lock({ signal }); } catch (error) { await session.close(); throw error; } const executionId = crypto.randomUUID(); let output; try { const ran = await runBrowserCode({ loader: options.loader, code: input.code, functions: session.functions, timeoutMs, signal }); const logs = ran.logs.length > 0 ? { logs: ran.logs } : {}; output = "error" in ran ? { status: "error", executionId, error: truncateResult(ran.error), ...logs } : { status: "completed", executionId, result: transformBrowserResult(ran.result), ...logs, ...ran.notice ? { notice: ran.notice } : {} }; } catch (error) { output = { status: "error", executionId, error: error instanceof Error ? error.message : String(error) }; } const report = await session.close(); const notice = [output.notice, ...report?.notices ?? []].filter(Boolean).join(" "); return { ...output, ...notice ? { notice } : {} }; } return { description, inputSchema: browserToolInputSchema, /** * Run `input` in `scope`, which defaults to the tool's `scope`. When * `signal` aborts, a run still waiting for the lock rejects with its * reason, and a running one stops with an error result. */ execute: (input, call = {}) => run(input, call.scope ?? defaultScope, call.signal) }; } //#endregion export { createBrowserToolCore as t }; //# sourceMappingURL=browser-tool-DOqT1oSD.js.map