UNPKG

agents

Version:

A home for your AI agents

255 lines (254 loc) • 10.9 kB
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