diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index bf6566c59..a003d90fa 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -117,6 +117,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 +324,16 @@ 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 + : undefined; const snapshot = await browserSnapshot(baseUrl, { format, targetId, limit, + maxChars, 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..5fb917ed8 100644 --- a/src/browser/pw-tools-core.ts +++ b/src/browser/pw-tools-core.ts @@ -10,6 +10,8 @@ import { let nextUploadArmId = 0; let nextDialogArmId = 0; +const MAX_SNAPSHOT_CHARS = 80_000; + function requireRef(value: unknown): string { const ref = typeof value === "string" ? value.trim() : ""; if (!ref) throw new Error("ref is required"); @@ -20,7 +22,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 +44,18 @@ export async function snapshotAiViaPlaywright(opts: { ), track: "response", }); - return { snapshot: String(result?.full ?? "") }; + const maxChars = opts.maxChars; + const limit = + typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 + ? Math.floor(maxChars) + : MAX_SNAPSHOT_CHARS; + let snapshot = String(result?.full ?? ""); + let truncated = false; + if (snapshot.length > limit) { + snapshot = `${snapshot.slice(0, limit)}\n\n[...TRUNCATED - page too large]`; + truncated = true; + } + return truncated ? { snapshot, truncated } : { snapshot }; } export async function clickViaPlaywright(opts: { diff --git a/src/browser/routes/agent.ts b/src/browser/routes/agent.ts index 3dbe13a82..052d9eab6 100644 --- a/src/browser/routes/agent.ts +++ b/src/browser/routes/agent.ts @@ -560,17 +560,30 @@ 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 = Number.isFinite(maxCharsRaw) ? maxCharsRaw : undefined; try { const tab = await profileCtx.ensureTabAvailable(targetId || undefined); if (format === "ai") { const pw = await requirePwAi(res, "ai snapshot"); if (!pw) return; + const resolvedMaxChars = + typeof maxChars === "number" && maxChars > 0 + ? maxChars + : typeof limit === "number" && limit > 0 + ? limit + : undefined; const snap = await pw.snapshotAiViaPlaywright({ cdpUrl: profileCtx.profile.cdpUrl, targetId: tab.targetId, + ...(resolvedMaxChars ? { maxChars: resolvedMaxChars } : {}), }); return res.json({ ok: true,