diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5313350d..aeff73191 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@
### Fixes
- Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias).
+- Agents/Browser: cap Playwright AI snapshots for tool calls (maxChars); CLI snapshots remain full. (#763) — thanks @thesash.
- Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete.
- CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete.
- Gateway/WebChat: include handshake validation details in the WebSocket close reason for easier debugging; preserve close codes.
diff --git a/README.md b/README.md
index 0d9c7f10f..365df020f 100644
--- a/README.md
+++ b/README.md
@@ -458,19 +458,20 @@ Special thanks to @andrewting19 for the Anthropic OAuth tool-name fix.
Thanks to all clawtributors:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/clawtributors-map.json b/scripts/clawtributors-map.json
index 5327ebbff..9dc6bb837 100644
--- a/scripts/clawtributors-map.json
+++ b/scripts/clawtributors-map.json
@@ -2,7 +2,8 @@
"ensureLogins": [
"jdrhyne",
"latitudeki5223",
- "manmal"
+ "manmal",
+ "thesash"
],
"seedCommit": "d6863f87",
"placeholderAvatar": "assets/avatar-placeholder.svg",
diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts
index bf6566c59..5ec8e84ac 100644
--- a/src/agents/tools/browser-tool.ts
+++ b/src/agents/tools/browser-tool.ts
@@ -44,6 +44,8 @@ const BROWSER_ACT_KINDS = [
type BrowserActKind = (typeof BROWSER_ACT_KINDS)[number];
+const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
+
// NOTE: Using a flattened object schema instead of Type.Union([Type.Object(...), ...])
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
// The discriminator (kind) determines which properties are relevant; runtime validates.
@@ -117,6 +119,7 @@ const BrowserToolSchema = Type.Object({
targetUrl: Type.Optional(Type.String()),
targetId: Type.Optional(Type.String()),
limit: Type.Optional(Type.Number()),
+ maxChars: Type.Optional(Type.Number()),
format: Type.Optional(Type.Union([Type.Literal("aria"), Type.Literal("ai")])),
fullPage: Type.Optional(Type.Boolean()),
ref: Type.Optional(Type.String()),
@@ -323,10 +326,21 @@ export function createBrowserTool(opts?: {
typeof params.limit === "number" && Number.isFinite(params.limit)
? params.limit
: undefined;
+ const maxChars =
+ typeof params.maxChars === "number" &&
+ Number.isFinite(params.maxChars) &&
+ params.maxChars > 0
+ ? Math.floor(params.maxChars)
+ : undefined;
+ const resolvedMaxChars =
+ format === "ai"
+ ? (maxChars ?? DEFAULT_AI_SNAPSHOT_MAX_CHARS)
+ : undefined;
const snapshot = await browserSnapshot(baseUrl, {
format,
targetId,
limit,
+ ...(resolvedMaxChars ? { maxChars: resolvedMaxChars } : {}),
profile,
});
if (snapshot.format === "ai") {
diff --git a/src/browser/client.ts b/src/browser/client.ts
index eb729ecb7..cb491b2e4 100644
--- a/src/browser/client.ts
+++ b/src/browser/client.ts
@@ -71,6 +71,7 @@ export type SnapshotResult =
targetId: string;
url: string;
snapshot: string;
+ truncated?: boolean;
};
export function resolveBrowserControlUrl(overrideUrl?: string) {
@@ -248,6 +249,7 @@ export async function browserSnapshot(
format: "aria" | "ai";
targetId?: string;
limit?: number;
+ maxChars?: number;
profile?: string;
},
): Promise {
@@ -255,6 +257,9 @@ export async function browserSnapshot(
q.set("format", opts.format);
if (opts.targetId) q.set("targetId", opts.targetId);
if (typeof opts.limit === "number") q.set("limit", String(opts.limit));
+ if (typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)) {
+ q.set("maxChars", String(opts.maxChars));
+ }
if (opts.profile) q.set("profile", opts.profile);
return await fetchBrowserJson(
`${baseUrl}/snapshot?${q.toString()}`,
diff --git a/src/browser/pw-ai.test.ts b/src/browser/pw-ai.test.ts
index d991913ed..ce3411b64 100644
--- a/src/browser/pw-ai.test.ts
+++ b/src/browser/pw-ai.test.ts
@@ -92,6 +92,28 @@ describe("pw-ai", () => {
expect(p2.session.detach).toHaveBeenCalledTimes(1);
});
+ it("truncates oversized snapshots", async () => {
+ const { chromium } = await import("playwright-core");
+ const longSnapshot = "A".repeat(20);
+ const p1 = createPage({ targetId: "T1", snapshotFull: longSnapshot });
+ const browser = createBrowser([p1.page]);
+
+ (
+ chromium.connectOverCDP as unknown as ReturnType
+ ).mockResolvedValue(browser);
+
+ const mod = await importModule();
+ const res = await mod.snapshotAiViaPlaywright({
+ cdpUrl: "http://127.0.0.1:18792",
+ targetId: "T1",
+ maxChars: 10,
+ });
+
+ expect(res.truncated).toBe(true);
+ expect(res.snapshot.startsWith("AAAAAAAAAA")).toBe(true);
+ expect(res.snapshot).toContain("TRUNCATED");
+ });
+
it("clicks a ref using aria-ref locator", async () => {
const { chromium } = await import("playwright-core");
const p1 = createPage({ targetId: "T1" });
diff --git a/src/browser/pw-tools-core.ts b/src/browser/pw-tools-core.ts
index 473bcc6f3..2f22d1380 100644
--- a/src/browser/pw-tools-core.ts
+++ b/src/browser/pw-tools-core.ts
@@ -20,7 +20,8 @@ export async function snapshotAiViaPlaywright(opts: {
cdpUrl: string;
targetId?: string;
timeoutMs?: number;
-}): Promise<{ snapshot: string }> {
+ maxChars?: number;
+}): Promise<{ snapshot: string; truncated?: boolean }> {
const page = await getPageForTargetId({
cdpUrl: opts.cdpUrl,
targetId: opts.targetId,
@@ -41,7 +42,17 @@ export async function snapshotAiViaPlaywright(opts: {
),
track: "response",
});
- return { snapshot: String(result?.full ?? "") };
+ let snapshot = String(result?.full ?? "");
+ const maxChars = opts.maxChars;
+ const limit =
+ typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0
+ ? Math.floor(maxChars)
+ : undefined;
+ if (limit && snapshot.length > limit) {
+ snapshot = `${snapshot.slice(0, limit)}\n\n[...TRUNCATED - page too large]`;
+ return { snapshot, truncated: true };
+ }
+ return { snapshot };
}
export async function clickViaPlaywright(opts: {
diff --git a/src/browser/routes/agent.ts b/src/browser/routes/agent.ts
index 3dbe13a82..d5b8a675e 100644
--- a/src/browser/routes/agent.ts
+++ b/src/browser/routes/agent.ts
@@ -560,8 +560,19 @@ export function registerBrowserAgentRoutes(
: (await getPwAiModule())
? "ai"
: "aria";
- const limit =
+ const limitRaw =
typeof req.query.limit === "string" ? Number(req.query.limit) : undefined;
+ const maxCharsRaw =
+ typeof req.query.maxChars === "string"
+ ? Number(req.query.maxChars)
+ : undefined;
+ const limit = Number.isFinite(limitRaw) ? limitRaw : undefined;
+ const maxChars =
+ typeof maxCharsRaw === "number" &&
+ Number.isFinite(maxCharsRaw) &&
+ maxCharsRaw > 0
+ ? Math.floor(maxCharsRaw)
+ : undefined;
try {
const tab = await profileCtx.ensureTabAvailable(targetId || undefined);
@@ -571,6 +582,7 @@ export function registerBrowserAgentRoutes(
const snap = await pw.snapshotAiViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl,
targetId: tab.targetId,
+ ...(maxChars ? { maxChars } : {}),
});
return res.json({
ok: true,