Browser: cap AI snapshots to avoid context overflow
This commit is contained in:
committed by
Peter Steinberger
parent
097e66391f
commit
d5d8c01dc7
@@ -117,6 +117,7 @@ const BrowserToolSchema = Type.Object({
|
|||||||
targetUrl: Type.Optional(Type.String()),
|
targetUrl: Type.Optional(Type.String()),
|
||||||
targetId: Type.Optional(Type.String()),
|
targetId: Type.Optional(Type.String()),
|
||||||
limit: Type.Optional(Type.Number()),
|
limit: Type.Optional(Type.Number()),
|
||||||
|
maxChars: Type.Optional(Type.Number()),
|
||||||
format: Type.Optional(Type.Union([Type.Literal("aria"), Type.Literal("ai")])),
|
format: Type.Optional(Type.Union([Type.Literal("aria"), Type.Literal("ai")])),
|
||||||
fullPage: Type.Optional(Type.Boolean()),
|
fullPage: Type.Optional(Type.Boolean()),
|
||||||
ref: Type.Optional(Type.String()),
|
ref: Type.Optional(Type.String()),
|
||||||
@@ -323,10 +324,16 @@ export function createBrowserTool(opts?: {
|
|||||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||||
? params.limit
|
? params.limit
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const maxChars =
|
||||||
|
typeof params.maxChars === "number" &&
|
||||||
|
Number.isFinite(params.maxChars)
|
||||||
|
? params.maxChars
|
||||||
|
: undefined;
|
||||||
const snapshot = await browserSnapshot(baseUrl, {
|
const snapshot = await browserSnapshot(baseUrl, {
|
||||||
format,
|
format,
|
||||||
targetId,
|
targetId,
|
||||||
limit,
|
limit,
|
||||||
|
maxChars,
|
||||||
profile,
|
profile,
|
||||||
});
|
});
|
||||||
if (snapshot.format === "ai") {
|
if (snapshot.format === "ai") {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export type SnapshotResult =
|
|||||||
targetId: string;
|
targetId: string;
|
||||||
url: string;
|
url: string;
|
||||||
snapshot: string;
|
snapshot: string;
|
||||||
|
truncated?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveBrowserControlUrl(overrideUrl?: string) {
|
export function resolveBrowserControlUrl(overrideUrl?: string) {
|
||||||
@@ -248,6 +249,7 @@ export async function browserSnapshot(
|
|||||||
format: "aria" | "ai";
|
format: "aria" | "ai";
|
||||||
targetId?: string;
|
targetId?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
maxChars?: number;
|
||||||
profile?: string;
|
profile?: string;
|
||||||
},
|
},
|
||||||
): Promise<SnapshotResult> {
|
): Promise<SnapshotResult> {
|
||||||
@@ -255,6 +257,9 @@ export async function browserSnapshot(
|
|||||||
q.set("format", opts.format);
|
q.set("format", opts.format);
|
||||||
if (opts.targetId) q.set("targetId", opts.targetId);
|
if (opts.targetId) q.set("targetId", opts.targetId);
|
||||||
if (typeof opts.limit === "number") q.set("limit", String(opts.limit));
|
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);
|
if (opts.profile) q.set("profile", opts.profile);
|
||||||
return await fetchBrowserJson<SnapshotResult>(
|
return await fetchBrowserJson<SnapshotResult>(
|
||||||
`${baseUrl}/snapshot?${q.toString()}`,
|
`${baseUrl}/snapshot?${q.toString()}`,
|
||||||
|
|||||||
@@ -92,6 +92,28 @@ describe("pw-ai", () => {
|
|||||||
expect(p2.session.detach).toHaveBeenCalledTimes(1);
|
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<typeof vi.fn>
|
||||||
|
).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 () => {
|
it("clicks a ref using aria-ref locator", async () => {
|
||||||
const { chromium } = await import("playwright-core");
|
const { chromium } = await import("playwright-core");
|
||||||
const p1 = createPage({ targetId: "T1" });
|
const p1 = createPage({ targetId: "T1" });
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
let nextUploadArmId = 0;
|
let nextUploadArmId = 0;
|
||||||
let nextDialogArmId = 0;
|
let nextDialogArmId = 0;
|
||||||
|
|
||||||
|
const MAX_SNAPSHOT_CHARS = 80_000;
|
||||||
|
|
||||||
function requireRef(value: unknown): string {
|
function requireRef(value: unknown): string {
|
||||||
const ref = typeof value === "string" ? value.trim() : "";
|
const ref = typeof value === "string" ? value.trim() : "";
|
||||||
if (!ref) throw new Error("ref is required");
|
if (!ref) throw new Error("ref is required");
|
||||||
@@ -20,7 +22,8 @@ export async function snapshotAiViaPlaywright(opts: {
|
|||||||
cdpUrl: string;
|
cdpUrl: string;
|
||||||
targetId?: string;
|
targetId?: string;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
}): Promise<{ snapshot: string }> {
|
maxChars?: number;
|
||||||
|
}): Promise<{ snapshot: string; truncated?: boolean }> {
|
||||||
const page = await getPageForTargetId({
|
const page = await getPageForTargetId({
|
||||||
cdpUrl: opts.cdpUrl,
|
cdpUrl: opts.cdpUrl,
|
||||||
targetId: opts.targetId,
|
targetId: opts.targetId,
|
||||||
@@ -41,7 +44,18 @@ export async function snapshotAiViaPlaywright(opts: {
|
|||||||
),
|
),
|
||||||
track: "response",
|
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: {
|
export async function clickViaPlaywright(opts: {
|
||||||
|
|||||||
@@ -560,17 +560,30 @@ export function registerBrowserAgentRoutes(
|
|||||||
: (await getPwAiModule())
|
: (await getPwAiModule())
|
||||||
? "ai"
|
? "ai"
|
||||||
: "aria";
|
: "aria";
|
||||||
const limit =
|
const limitRaw =
|
||||||
typeof req.query.limit === "string" ? Number(req.query.limit) : undefined;
|
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 {
|
try {
|
||||||
const tab = await profileCtx.ensureTabAvailable(targetId || undefined);
|
const tab = await profileCtx.ensureTabAvailable(targetId || undefined);
|
||||||
if (format === "ai") {
|
if (format === "ai") {
|
||||||
const pw = await requirePwAi(res, "ai snapshot");
|
const pw = await requirePwAi(res, "ai snapshot");
|
||||||
if (!pw) return;
|
if (!pw) return;
|
||||||
|
const resolvedMaxChars =
|
||||||
|
typeof maxChars === "number" && maxChars > 0
|
||||||
|
? maxChars
|
||||||
|
: typeof limit === "number" && limit > 0
|
||||||
|
? limit
|
||||||
|
: undefined;
|
||||||
const snap = await pw.snapshotAiViaPlaywright({
|
const snap = await pw.snapshotAiViaPlaywright({
|
||||||
cdpUrl: profileCtx.profile.cdpUrl,
|
cdpUrl: profileCtx.profile.cdpUrl,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
|
...(resolvedMaxChars ? { maxChars: resolvedMaxChars } : {}),
|
||||||
});
|
});
|
||||||
return res.json({
|
return res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user