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:

- steipete joaohlisboa mneves75 rahthakor joshp123 mukhtharcm maxsumrall xadenryan Tobias Bischoff hsrvc - magimetal jamesgroat NicholasSpisak dantelex daveonkels radek-paclt Eng. Juan Combetto Mariano Belinky julianengel claude - jeffersonwarrior sreekaransrinath dbhurley gupsammy nachoiacovino Vasanth Rao Naik Sabavat lc0rp scald andranik-sahakyan nachx639 - sircrumpet rafaelreis-r meaningfool ratulsarna lutr0 abhisekbasu1 emanuelst thewilloftheshadow KristijanJovanovski osolmaz - kiranjd sebslight onutc CashWilliams sheeek manuelhettich minghinmatthewlam buddyh mcinteerj timkrase - azade-c Yurii Chukhlib austinm911 blacksmith-sh[bot] imfing jarvis-medmatic mahmoudashraf93 petter-b RandyVentures jalehman - jonasjancarik obviyus dan-dr iamadig koala73 manmal neist ogulcancelik pasogott VACInc - zats antons Django Navarro L36 Server pcty-nextgen-service-account Syhids erik-agens erikpr1994 fcatuhe HeimdallStrategy - henrino3 jayhickey Jonathan D. Rhyne (DJ-D) jverdi Keith the Silly Goose Kit mitschabaude-bot ngutman oswalpalash p6l-richard - philipp-spiess pkrmf Sash Catanzarite VAC adam91holt alejandro maza andrewting19 Asleep123 bjesuiter bolismauro - cash-echo-bot Clawd conhecendocontato gtsifrikas HazAT hrdwdmrbl hugobarauna Jarvis kitze kkarimi - levifig Lloyd loukotal martinpucik mickahouan Miles mrdbstn MSch nexty5870 prathamdby - reeltimeapps RLTCmpe Rolf Fredheim rubyrunsstuff Samrat Jha snopoke wes-davis wstock YuriNachos Zach Knickerbocker - zknicker Azade ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani pcty-nextgen-ios-builder Quentin Randy Torres - William Stock + steipete joaohlisboa mneves75 rahthakor joshp123 mukhtharcm maxsumrall xadenryan Tobias Bischoff magimetal + NicholasSpisak hsrvc claude jamesgroat dantelex daveonkels radek-paclt jeffersonwarrior mteam88 Eng. Juan Combetto + Mariano Belinky julianengel benithors sreekaransrinath dbhurley gupsammy cristip73 nachoiacovino Vasanth Rao Naik Sabavat lc0rp + scald andranik-sahakyan nachx639 sircrumpet peschee rafaelreis-r meaningfool ratulsarna lutr0 abhisekbasu1 + thewilloftheshadow emanuelst KristijanJovanovski osolmaz kiranjd Sebastian Barrios sheeek onutc CashWilliams manuelhettich + minghinmatthewlam myfunc buddyh mcinteerj timkrase azade-c obviyus bjesuiter superman32432432 Yurii Chukhlib + antons austinm911 blacksmith-sh[bot] imfing jarvis-medmatic mahmoudashraf93 petter-b pkrmf RandyVentures dan-dr + HeimdallStrategy jalehman jonasjancarik neist iamadig koala73 manmal ogulcancelik pasogott petradonka + VACInc zats Chris Taylor Django Navarro gabriel-trigo Kit L36 Server ngutman pcty-nextgen-service-account rubyrunsstuff + Syhids danielz1z erik-agens erikpr1994 fcatuhe henrino3 jayhickey Jonathan D. Rhyne (DJ-D) juanpablodlc jverdi + Keith the Silly Goose mitschabaude-bot mjrussell oswalpalash p6l-richard philipp-spiess Sash Catanzarite VAC adam91holt alejandro maza + andrewting19 Asleep123 bolismauro cash-echo-bot Clawd conhecendocontato evalexpr gtsifrikas HazAT hrdwdmrbl + hugobarauna Jarvis kitze kkarimi levifig Lloyd loukotal martinpucik mickahouan Miles + mrdbstn MSch nexty5870 prathamdby reeltimeapps RLTCmpe Rolf Fredheim roshanasingh4 Samrat Jha snopoke + The Admiral wes-davis wstock YuriNachos Zach Knickerbocker zknicker Azade ddyo Erik latitudeki5223 + Manuel Maly Mourad Boustani pcty-nextgen-ios-builder Quentin Randy Torres thesash William Stock

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,