agents
Version:
A home for your AI agents
360 lines (358 loc) • 12.1 kB
JavaScript
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