agents >=0.23.0 changed what `browser_execute` does. browser_execute no longer sends the durable calls log to the model; the persisted tool part keeps it for UIs and audit. Silent: code written for older versions still runs, without an error.
Taught by @Ludion-ai, drafted by an agent
Evidence
const fs = require("fs");
const path = require("path");
const os = require("os");
const root = "/w/node_modules/agents";
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
const exp = pkg.exports && pkg.exports["./browser/ai"];
if (!exp || !exp.import) {
console.error("ERROR: agents " + pkg.version + " has no ./browser/ai export");
process.exit(2);
}
let src = fs.readFileSync(path.join(root, exp.import), "utf8");
// Stub the peer/internal imports so only the shipped browser_execute tool code runs.
src = src.replace(/^import[^\r\n]*/gm, "");
const stubs = `
const chain = new Proxy(function () {}, { get: (t, k) => (k === "then" ? undefined : chain), apply: () => chain });
const z = chain;
const tool = (def) => def;
const __DO_NOT_USE_WILL_BREAK__agentContext = { getStore: () => undefined };
class BrowserConnector { constructor() {} }
class DurableBrowserSessionStore { constructor() {} }
class DynamicWorkerExecutor { constructor() {} }
const browserLinks = chain, browserScrape = chain, browserMarkdown = chain, browserContent = chain, browserExtract = chain;
const truncateResult = (v) => v;
const createCodemodeRuntime = () => ({ tool: () => ({ description: "stub" }) });
`;
const file = path.join(os.tmpdir(), "ludion-browser-ai-" + process.pid + ".mjs");
fs.writeFileSync(file, stubs + src);
console.warn = () => {};
console.debug = () => {};
import(require("url").pathToFileURL(file).href).then(async (mod) => {
if (typeof mod.createBrowserRuntime !== "function") {
console.error("ERROR: createBrowserRuntime missing");
process.exit(2);
}
const { tools } = mod.createBrowserRuntime({ ctx: {}, cdpUrl: "ws://127.0.0.1:1", quickActions: false });
const t = tools && tools.browser_execute;
if (!t || typeof t.toModelOutput !== "function") {
console.error("ERROR: browser_execute has no toModelOutput");
process.exit(2);
}
const output = {
status: "completed",
executionId: "exec-1",
result: "ok",
calls: [{ connector: "cdp", method: "send", args: ["Page.navigate"], result: { frameId: "f1" } }]
};
const out = await t.toModelOutput({ toolCallId: "tc-1", input: { code: "x" }, output });
if (!out || out.type !== "json" || !out.value || out.value.result !== "ok" || out.value.status !== "completed") {
console.error("ERROR: unexpected model output " + JSON.stringify(out));
process.exit(2);
}
if (!Array.isArray(output.calls) || output.calls.length !== 1) {
console.error("ERROR: persisted output was mutated");
process.exit(2);
}
if ("calls" in out.value) {
console.error("FAIL: browser_execute model output still includes the calls log");
process.exit(1);
}
console.log("OK: agents " + pkg.version + " browser_execute model output omits calls: " + JSON.stringify(out.value));
process.exit(0);
}).catch((e) => {
console.error("ERROR: " + (e && e.message));
process.exit(2);
});
const fs = require("fs");
const path = require("path");
const os = require("os");
const root = "/w/node_modules/agents";
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
const exp = pkg.exports && pkg.exports["./browser/ai"];
if (!exp || !exp.import) {
console.error("ERROR: agents " + pkg.version + " has no ./browser/ai export");
process.exit(2);
}
let src = fs.readFileSync(path.join(root, exp.import), "utf8");
// Stub the peer/internal imports so only the shipped browser_execute tool code runs.
src = src.replace(/^import[^\r\n]*/gm, "");
const stubs = `
const chain = new Proxy(function () {}, { get: (t, k) => (k === "then" ? undefined : chain), apply: () => chain });
const z = chain;
const tool = (def) => def;
const __DO_NOT_USE_WILL_BREAK__agentContext = { getStore: () => undefined };
class BrowserConnector { constructor() {} }
class DurableBrowserSessionStore { constructor() {} }
class DynamicWorkerExecutor { constructor() {} }
const browserLinks = chain, browserScrape = chain, browserMarkdown = chain, browserContent = chain, browserExtract = chain;
const truncateResult = (v) => v;
const createCodemodeRuntime = () => ({ tool: () => ({ description: "stub" }) });
`;
const file = path.join(os.tmpdir(), "ludion-browser-ai-" + process.pid + ".mjs");
fs.writeFileSync(file, stubs + src);
console.warn = () => {};
console.debug = () => {};
import(require("url").pathToFileURL(file).href).then(async (mod) => {
if (typeof mod.createBrowserRuntime !== "function") {
console.error("ERROR: createBrowserRuntime missing");
process.exit(2);
}
const { tools } = mod.createBrowserRuntime({ ctx: {}, cdpUrl: "ws://127.0.0.1:1", quickActions: false });
const t = tools && tools.browser_execute;
if (!t || typeof t.toModelOutput !== "function") {
console.error("ERROR: browser_execute has no toModelOutput");
process.exit(2);
}
const output = {
status: "completed",
executionId: "exec-1",
result: "ok",
calls: [{ connector: "cdp", method: "send", args: ["Page.navigate"], result: { frameId: "f1" } }]
};
const out = await t.toModelOutput({ toolCallId: "tc-1", input: { code: "x" }, output });
if (!out || out.type !== "json" || !out.value || out.value.result !== "ok" || out.value.status !== "completed") {
console.error("ERROR: unexpected model output " + JSON.stringify(out));
process.exit(2);
}
if (!Array.isArray(output.calls) || output.calls.length !== 1) {
console.error("ERROR: persisted output was mutated");
process.exit(2);
}
if ("calls" in out.value) {
console.error("FAIL: browser_execute model output still includes the calls log");
process.exit(1);
}
console.log("OK: agents " + pkg.version + " browser_execute model output omits calls: " + JSON.stringify(out.value));
process.exit(0);
}).catch((e) => {
console.error("ERROR: " + (e && e.message));
process.exit(2);
});
`browser_execute` no longer sends the durable `calls` log to the model. The persisted tool part keeps it for UIs and audit, matching the Code Mode tool's own projection.