agents
Version:
A home for your AI agents
255 lines (254 loc) • 10.9 kB
JavaScript
import { t as __DO_NOT_USE_WILL_BREAK__agentContext } from "./current-agent-DhoDkSnH.js";
import "./session-CRY0bGiI.js";
import { t as redactBase64Payloads } from "./base64-redaction-C1Uu29t5.js";
//#region src/browser/truncate.ts
/**
* Result truncation for the browser tools, copied from
* `@cloudflare/codemode`'s `truncateResult` so `agents/browser` doesn't need
* codemode. Internal — not an entry point.
*
* Structured values are truncated structurally: the output is always valid
* JSON of the same shape, with the largest values cut first. A truncated
* string ends with a marker, a truncated array ends with a marker element
* counting the dropped items, and an object that had to lose entries carries a
* marker entry naming the omitted keys. Every marker contains
* `--- TRUNCATED ---` so callers and models can find them.
*/
/** ~4 characters per token is a reasonable cross-model estimate. */
const CHARS_PER_TOKEN = 4;
const DEFAULT_MAX_TOKENS = 6e3;
const TRUNCATION_MARKER = "--- TRUNCATED ---";
/**
* The least a string or container is worth keeping at (see `floorSize`). A
* value that would get fewer serialized characters than this is dropped in
* favour of its siblings rather than reduced to a bare marker.
*/
const MIN_SLOT = 128;
function budget(options) {
const maxTokens = options?.maxTokens ?? DEFAULT_MAX_TOKENS;
return {
maxChars: options?.maxChars ?? maxTokens * CHARS_PER_TOKEN,
maxTokens
};
}
/**
* Truncate a text response to a character budget, appending a marker that notes
* the original size so the model knows the output was clipped. Returns the
* input unchanged when it is within budget.
*/
function truncateResponse(text, options) {
const { maxChars, maxTokens } = budget(options);
if (text.length <= maxChars) return text;
const estimatedTokens = Math.ceil(text.length / CHARS_PER_TOKEN);
return text.slice(0, maxChars) + `\n\n${TRUNCATION_MARKER}\nResponse was ~${estimatedTokens.toLocaleString()} tokens (limit: ${maxTokens.toLocaleString()}). Narrow the request to reduce response size.`;
}
/**
* Truncate a structured result. Strings are truncated directly. Other values
* pass through unchanged (same reference) when their JSON serialization is
* within budget. When oversized, the value is shrunk structurally — largest
* values first, deepest last — so the model gets a bounded value that is still
* valid JSON of the original shape, with markers where content was cut.
*
* Values that can't be serialized (cycles, bigint, `undefined`) are returned
* unchanged.
*/
function truncateResult(value, options) {
if (typeof value === "string") return truncateResponse(value, options);
let serialized;
try {
serialized = JSON.stringify(value);
} catch {
return value;
}
if (serialized === void 0) return value;
const { maxChars } = budget(options);
if (serialized.length <= maxChars) return value;
const shrunk = shrink(JSON.parse(serialized), maxChars);
return size(shrunk) <= maxChars ? shrunk : truncateResponse(serialized, options);
}
/** Serialized size in characters (compact JSON). */
function size(value) {
return JSON.stringify(value).length;
}
/** Only strings and containers can give up characters; scalars are atomic. */
function shrinkable(value) {
return typeof value === "string" || typeof value === "object" && !!value;
}
/**
* The least budget a value is kept at; a scalar must fit whole. The floor
* never exceeds half the container's own budget, so tiny budgets still keep
* something rather than nothing.
*/
function floorSize(value, maxChars) {
const full = size(value);
return shrinkable(value) ? Math.min(full, MIN_SLOT, Math.max(2, Math.floor(maxChars / 2))) : full;
}
function shrink(value, maxChars) {
if (size(value) <= maxChars) return value;
if (typeof value === "string") return shrinkString(value, maxChars);
if (Array.isArray(value)) return shrinkArray(value, maxChars);
if (typeof value === "object" && value !== null) return shrinkObject(value, maxChars);
return value;
}
function shrinkString(value, maxChars) {
const suffix = ` ${TRUNCATION_MARKER} ${value.length.toLocaleString()} chars`;
let keep = Math.max(0, maxChars - suffix.length - 2);
let out = value.slice(0, keep) + suffix;
while (keep > 0 && size(out) > maxChars) {
keep = Math.max(0, keep - (size(out) - maxChars));
out = value.slice(0, keep) + suffix;
}
return size(out) <= maxChars ? out : TRUNCATION_MARKER.slice(0, Math.max(0, maxChars - 2));
}
/**
* Share `available` characters across values by water-filling: values are
* admitted smallest first, each taking the lesser of its full size and an
* equal share of what is left. Requires `available >= Σ floorSize(values)`;
* then every value receives at least its floor.
*/
function allocate(values, available) {
const sizes = values.map(size);
const order = sizes.map((_, i) => i).sort((a, b) => sizes[a] - sizes[b]);
const allocation = new Array(values.length);
let remaining = available;
let count = values.length;
for (const i of order) {
const alloc = Math.min(sizes[i], Math.floor(remaining / count));
allocation[i] = alloc;
remaining -= alloc;
count--;
}
return allocation;
}
function shrinkArray(items, maxChars) {
const marker = (dropped) => `${TRUNCATION_MARKER} ${dropped.toLocaleString()} more items`;
const floors = items.map((item) => floorSize(item, maxChars));
const skeleton = (count) => 2 + Math.max(0, count - 1);
let keep = items.length;
if (floors.reduce((n, f) => n + f, 0) + skeleton(keep) > maxChars) {
const tail = size(marker(items.length)) + 1;
let used = skeleton(0) + tail;
keep = 0;
while (keep < items.length && used + floors[keep] + 1 <= maxChars) {
used += floors[keep] + 1;
keep++;
}
}
const kept = items.slice(0, keep);
const dropped = items.length - keep;
const tail = dropped > 0 ? size(marker(dropped)) + (keep > 0 ? 1 : 0) : 0;
const allocation = allocate(kept, maxChars - skeleton(keep) - tail);
const out = kept.map((item, i) => shrink(item, allocation[i]));
if (dropped > 0 && size([...out, marker(dropped)]) <= maxChars) out.push(marker(dropped));
return out;
}
function shrinkObject(value, maxChars) {
const entries = Object.entries(value);
let markerKey = TRUNCATION_MARKER;
while (markerKey in value) markerKey += " ";
const keyCost = (key) => size(key) + 1;
const floors = entries.map(([k, v]) => keyCost(k) + floorSize(v, maxChars));
const floorOf = (chars) => Math.min(chars, MIN_SLOT, Math.max(2, Math.floor(maxChars / 2)));
const byValueSize = entries.map((_, i) => i).sort((a, b) => size(entries[b][1]) - size(entries[a][1]));
const dropped = /* @__PURE__ */ new Set();
let present = entries.length;
let floorsSum = floors.reduce((n, f) => n + f, 0);
let namesLength = 0;
const noteLength = () => `${dropped.size.toLocaleString()} keys omitted: `.length + namesLength + 2 * (dropped.size - 1);
const cost = () => {
const marker = dropped.size > 0 ? keyCost(markerKey) + floorOf(noteLength() + 2) : 0;
const count = present + (dropped.size > 0 ? 1 : 0);
return 2 + Math.max(0, count - 1) + floorsSum + marker;
};
for (const i of byValueSize) {
if (cost() <= maxChars) break;
dropped.add(i);
present--;
floorsSum -= floors[i];
namesLength += size(entries[i][0]) - 2;
}
const kept = entries.filter((_, i) => !dropped.has(i));
const omitted = [...dropped].sort((a, b) => a - b).map((i) => entries[i][0]);
const note = `${omitted.length.toLocaleString()} keys omitted: ${omitted.join(", ")}`;
if (cost() > maxChars) {
const room = maxChars - 2 - keyCost(markerKey);
return room >= 2 ? { [markerKey]: shrink(note, room) } : {};
}
const all = [...kept];
if (omitted.length > 0) all.push([markerKey, note]);
const fixed = 2 + Math.max(0, all.length - 1) + all.reduce((n, [k]) => n + keyCost(k), 0);
const allocation = allocate(all.map(([, v]) => v), maxChars - fixed);
const out = {};
all.forEach(([k, v], i) => {
out[k] = shrink(v, allocation[i]);
});
return out;
}
//#endregion
//#region src/browser/tool-helpers.ts
/** What `Page.captureScreenshot` can produce. */
const SCREENSHOT_MEDIA_TYPES = /* @__PURE__ */ new Set([
"image/png",
"image/jpeg",
"image/webp"
]);
const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
/**
* The screenshot the model's code returned, or `null` if it didn't return
* one. The model writes this shape itself, so only an image type, base64
* data, and the session's size limit count: anything else is treated as a
* plain result and cut down like one, rather than kept whole or sent to the
* model as an image.
*/
function browserScreenshotOutput(value) {
const outer = typeof value === "object" && value !== null ? value : null;
const result = outer && typeof outer.result === "object" && outer.result !== null ? outer.result : outer;
if (result?.type !== "browser_screenshot" || typeof result.mediaType !== "string" || typeof result.data !== "string" || !SCREENSHOT_MEDIA_TYPES.has(result.mediaType) || result.data.length > 1e6 || !BASE64.test(result.data)) return null;
return result;
}
function transformBrowserResult(value) {
return browserScreenshotOutput(value) ? value : truncateResult(redactBase64Payloads(value));
}
/**
* @param notes Sentences appended when a screenshot collapses the output to
* text, so notices on the output aren't lost with it.
*/
function browserExecuteModelOutput(output, notes = []) {
const screenshot = browserScreenshotOutput(output);
if (screenshot) {
const approximateBytes = Math.floor(screenshot.data.length * 3 / 4);
return {
type: "text",
value: [`Screenshot captured successfully (${screenshot.mediaType}, approximately ${approximateBytes.toLocaleString()} bytes); the image is kept for the UI and omitted here.`, ...notes].join(" ")
};
}
const redacted = redactBase64Payloads(typeof output === "object" && output !== null && !Array.isArray(output) ? (({ calls: _calls, ...rest }) => ({
...rest,
...Array.isArray(rest.logs) ? { logs: truncateResult(rest.logs) } : {}
}))(output) : output);
try {
const serialized = JSON.stringify(redacted);
return {
type: "json",
value: serialized === void 0 ? null : JSON.parse(serialized)
};
} catch {
return {
type: "text",
value: "Browser execution completed, but its result could not be serialized for model context."
};
}
}
/**
* 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;
}
//#endregion
export { truncateResult as a, transformBrowserResult as i, browserScreenshotOutput as n, resolveCtx as r, browserExecuteModelOutput as t };
//# sourceMappingURL=tool-helpers-F3_Wy2YX.js.map