Add opt-in playwright_execute tool to the CUA agent and CLI#33
Conversation
Exposes a tool that runs Playwright/TypeScript directly against the browser session (via the Kernel SDK browsers.playwright.execute) for steps that are awkward as raw pointer/keyboard actions. Modeled on the existing computer_use_extra navigation tool: defined in cua-ai, executed through the translator, gated by a `playwright` option, and added to keepToolNames so providers retain it in the payload. Enable with the `--playwright` CLI flag. Returns result/stdout/stderr and appends a fresh screenshot so the screenshot loop stays coherent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drop misleading "Defaults to 60" from timeout_sec description; the actual default lives in the Kernel SDK, not here. - Expose result/stdout/stderr/error on PlaywrightDetails so library consumers can branch on the structured execution result without re-parsing tool content text. - Guard formatPlaywrightResult against non-JSON-serializable returns (e.g. BigInt, circular refs) so a successful Playwright run never becomes a tool-level error. - Sync package-lock.json to match the cua-cli 0.1.1 bump in a7cdc07. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Locals don't persist across calls but the browser session does. Without this, a model could write code in call N assuming variables from call N-1 are still in scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Earlier review feedback dropped "Defaults to 60" out of a worry that the default lived in the SDK and could drift. The kernel.sh docs put both the default (60s) and the cap (300s) on the server, so the description is the authoritative place to surface them — the model can't choose a sensible timeout without that anchor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Schema description tells the model "max 300" but nothing enforced it. A model that ignored the bound would have hit a confusing SDK-level failure depending on server behavior; this clamp keeps the client honest to the documented contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- packages/agent: list playwright option alongside computerUseExtra and add a paragraph explaining the tool's behavior and tested-models scope. - packages/ai: list the new tool-definition factory, schema, constants, and CuaPlaywrightInput type in the API surface index. - packages/cli: document --playwright with a short explainer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
formatPlaywrightResult's JSON.stringify try/catch guarded against non-serializable values, but execution.result came from the SDK after a JSON round trip through the wire — anything that survived that is already JSON-safe, so the catch arm is unreachable. The executePlaywright timeout chain checked typeof === "number" (dead, the parameter is TS-typed number | undefined) and Number.isFinite (redundant — timeoutSec > 0 already rejects NaN, and Math.min handles Infinity). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Empirical results show CUA-specialized providers (Tzafon, Yutori) do emit playwright_execute calls — earlier docs were overly cautious. Yutori in particular demonstrates the failure-as-content design well: it iterated through two wrong-API attempts (page.querySelector, bare document) before reading the stderr/error blocks and landing on page.evaluate(), which throwing would have prevented. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Firetiger deploy monitoring skipped This PR didn't match the auto-monitor filter configured on your GitHub connection:
Reason: PR is in the To monitor this PR anyway, reply with |
Matches executeBatchTool's shape: the trailing translator.screenshot() lives inside the same try/catch as the underlying work, so any failure in the pipeline produces a single wrapped tool error rather than diverging based on which step failed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 565fe01. Configure here.
- executePlaywright: timeout_sec values below 1s previously truncated to 0 and were forwarded to the SDK, which differs from omitting the field. Floor the truncated value at 1s; anything sub-second falls back to "use server default". - Document PlaywrightDetails fields so library consumers know what each one means without reading the executor source. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rgarcia
left a comment
There was a problem hiding this comment.
reviewed end to end — solid work, and a faithful mirror of the computer_use_extra pattern. the failure-as-content design and the keepToolNames() wiring so tzafon/yutori don't strip the tool are the right calls. one change i'd want before merge, one nit, and two follow-ups.
change request
packages/agent/src/tools.ts:222 — drop the auto-appended screenshot from playwright_execute. the native action / computer_batch tools leave screenshots to the model (batch only appends one as a fallback when nothing was read); playwright instead copied navigation's always-append. but playwright is the one tool that's frequently a pure read (return await page.title()), where forcing a screenshot every call is wasted image tokens + latency. better to let the model pull a screenshot on a follow-up turn. the content.length === 0 → statusText fallback at tools.ts:220 already keeps content non-empty for side-effect-only calls, so the function itself needs nothing else. things to keep in sync:
packages/agent/README.md:135— "a fresh screenshot is appended after every call…" becomes false; reword toward "request a screenshot on a follow-up turn to see the page."packages/ai/src/providers/common.ts:346— already says "capture page state with a follow-up screenshot action," which stays correct; worth tightening to make explicit that none is returned automatically.packages/agent/test/tool-exhaustiveness.test.ts:116,140— both assertcontent.at(-1)is an image; that becomes the trailing text block. thecaptureScreenshotmocks at:102,127go unused for these two. while in here, add a side-effect-only case (no result/stdout/stderr, success →statusText), since that's now the primary content shape.
nit
packages/agent/src/tools.ts:209 vs :227 — stdout/stderr guards diverge. model-facing content gates on execution.stdout?.trim() and stores the trimEnd()-ed value, while details.stdout gates on plain truthiness and stores the raw value. so a whitespace-only stdout lands in details but not in the model content. probably intentional (details = faithful capture), just flagging the inconsistency against the PlaywrightDetails doc ("present only when the daemon captured output"). same for stderr.
follow-ups (separate PRs, not blocking)
- same screenshot removal for
computer_use_extra(tools.ts:175,executeNavigationTool) — it also unconditionally appends. - a
/playwright [on|off]interactive slash command to toggle the tool mid-session, mirroring/model'ssetModel→state.toolsrefresh (agent.ts:306-310). the livethis.options.playwrightreads intools()/keepToolNames()make this cheap; needs asetPlaywrightmutator on runtime + agent + harness, aslash-commands.tsentry/parse case, and anapplyPlaywrightCommandhandler inmain.ts.
…/stderr details playwright_execute is frequently a pure read where forcing a screenshot wastes image tokens and latency. Let the model request one on a follow-up turn. The existing content.length === 0 → statusText fallback keeps content non-empty for side-effect-only calls. Also tighten the PlaywrightDetails TSDoc for stdout/stderr to reflect that details captures raw daemon output (potentially whitespace-only), while the model-facing content blocks only surface trimmed non-empty output. - packages/agent/src/tools.ts: drop screenshot append in executePlaywrightTool; update PlaywrightDetails TSDoc for stdout/stderr. - packages/agent/README.md and packages/ai/src/providers/common.ts: reword to make explicit no screenshot is returned automatically. - packages/agent/test/tool-exhaustiveness.test.ts: flip the trailing-image assertions to assert no image is appended; drop the unused captureScreenshot mocks; add a side-effect-only case that hits the statusText fallback.
|
@rgarcia requested change + nit addressed f855cf1 and reverified all tests still run. @masnwilliams lmk if you have any thoughts before I merge. If they are optimizations that can be added later, happy to address as part of Raf's suggested follow up items too. |
rgarcia
left a comment
There was a problem hiding this comment.
re-reviewed at f855cf1 — the screenshot-removal change request is fully addressed, and cleanly.
tools.ts— auto-screenshot dropped fromplaywright_execute; thecontent.length === 0→statusTextfallback now carries side-effect-only calls, so content is never empty. ✅packages/agent/README.md/packages/ai/src/providers/common.ts— both reworded to "no screenshot is returned automatically; request one on a follow-up turn." accurate now. ✅- tests — happy-path renamed and the two exec tests now assert no image block, the dead
captureScreenshotmocks are gone, and you added the side-effect-only case (asserts content is exactly thestatusText). ✅ - stdout/stderr nit — resolved by aligning the
PlaywrightDetailsdoc to the actual behavior ("raw daemon output … may be whitespace-only") rather than changing the guards. good call keepingdetailsas the faithful raw capture andcontentas the trimmed view.
verified locally: @onkernel/cua-agent suite green (25 passed, incl. the new side-effect-only test) and tsc -b typecheck clean.
the two follow-ups — same screenshot removal for computer_use_extra (navigation), and a /playwright [on|off] mid-session toggle — remain tracked as separate PRs, not blocking. nothing else outstanding from my end.
rgarcia
left a comment
There was a problem hiding this comment.
approving — screenshot-removal change request fully addressed at f855cf1, stdout/stderr doc nit resolved, and i verified the agent suite (25 passing) + tsc -b typecheck locally. the two follow-ups (navigation screenshot removal, /playwright mid-session toggle) are tracked as separate non-blocking PRs.

Summary
Adds an opt-in
playwright_executetool so the model can run Playwright/TypeScript directly against the live browser session — for steps that are awkward as raw pointer/keyboard actions (precise DOM reads, form fills, data extraction, waiting on selectors). It sits alongside the existing computer-use tools rather than replacing them.Execution runs server-side in the browser VM via the Kernel SDK (
client.browsers.playwright.execute), which exposespage/context/browserand lets the codereturna JSON-serializable value. No CDP wiring or local Playwright is needed.It is modeled directly on the existing
computer_use_extranavigation tool:@onkernel/cua-ai—playwright_executetool name,{ code, timeout_sec? }schema, andcreateCuaPlaywrightToolDefinition().@onkernel/cua-agent—InternalComputerTranslator.executePlaywright(), aplaywrightexecutor intools.ts, and theplaywrightoption threaded throughCuaAgent/CuaAgentHarness. The tool name is added tokeepToolNames()so provider payload hooks don't strip it.@onkernel/cua-cli—--playwrightflag and a TUI tool-call preview.Behavior, per the decisions on this:
--playwright(CLI) orplaywright: true(library).result(when present), plusstdout/stderronly when non-empty, anderroronsuccess: false. A reported failure comes back as tool content (not thrown) so the model can adapt; only a thrown SDK error surfaces as a tool error. Library consumers can also read the structuredresult/stdout/stderr/erroroffPlaywrightDetailswithout re-parsing tool content text.timeout_secfollows the documented server contract (default 60s, max 300s); values are clamped client-side so the model can't violate the cap.Naming note
The model-facing wire name is
playwright_execute(snake_case, consistent withcomputer_use_extra/computer_batch), the CLI flag is--playwright, and the option isplaywright.Model support
The tool is advertised as a generic function tool, so any provider that supports function calling alongside its native computer-use API can call it. The
playwright_executename is added tokeepToolNames()so provider payload hooks that filter unknown tools (tzafon/yutori) won't strip it. Verified e2e against:claude-opus-4-7)tzafon.northstar-cua-fast-1.6)n1.5-latest)OpenAI (
gpt-5.5) and Google (gemini-3-flash-preview) are unit-tested but not yet e2e-verified against a live browser.Docs
packages/agent/README.md,packages/ai/README.md, andpackages/cli/README.mdupdated alongside the code.Test plan
npm run typecheck(workspace) passes@onkernel/cua-agentsuite passes, incl. 3 new tests (tool synthesized when enabled; execution formats result/stdout + appends screenshot; failure surfaces as content without throwing)@onkernel/cua-ai(88) and@onkernel/cua-cli(37) suites passcua --playwright) on three providers:claude-opus-4-7) — happy path returnedresult: {"h1":"Example Domain","title":"Example Domain"}in one turn; details carried the structuredresultobject.tzafon.northstar-cua-fast-1.6) — same one-turn happy path. ConfirmskeepToolNames()correctly preserves the tool through tzafon's payload hook.n1.5-latest) — recovered from aTypeError(page.querySelectoris not a function) and aReferenceError(documentnot defined) by reading the failure-as-contentstderr/errorblocks, then arrived at the correctpage.evaluate(...)pattern. Confirms the failure-as-content design closes the iteration loop.success: falsewith the Playwrightstderr/errorcame back as tool content (not thrown), screenshot still appended, model read it and adapted.🤖 Generated with Claude Code
Note
Medium Risk
Introduces server-side arbitrary Playwright execution against live browser sessions when enabled; mitigated by opt-in default, timeout caps, and soft failure handling for model recovery.
Overview
Adds an opt-in
playwright_executetool so models can run Playwright/TypeScript against the live Kernel browser session (DOM reads, selectors, form fills) alongside existing computer-use tools.@onkernel/cua-aidefines the tool (CuaPlaywrightSchema,createCuaPlaywrightToolDefinition(),CUA_PLAYWRIGHT_TOOL_NAME).@onkernel/cua-agentwires it through a newplaywright?: booleanonCuaAgent/CuaAgentHarness,InternalComputerTranslator.executePlaywright()(Kernelbrowsers.playwright.execute, optionaltimeout_secclamped to 300s), and executor logic that returns structuredPlaywrightDetailsplus model-facing text forresult/stdout/stderr/error. Reported Playwright failures are tool content (not thrown); only SDK errors throw.playwright_executeis included inkeepToolNames()so Yutori/Tzafon payload hooks do not strip it. Unlike navigation/batch tools, this path does not auto-append a screenshot.@onkernel/cua-cliadds--playwright, passes it into the harness, and shows truncated code in the TUI tool-call preview. README updates and unit tests cover synthesis, success/failure shapes, and no image on success.Reviewed by Cursor Bugbot for commit f855cf1. Bugbot is set up for automated code reviews on this repo. Configure here.