Skip to content

browser run: BROWSER_RUN_API_UNSUPPORTED names the missing API but never the supported alternative #336

Description

@ankitranjan7

Summary

BROWSER_RUN_API_UNSUPPORTED tells the caller what does not work and stops there. It never names the thing that does. Every occurrence costs an agent at least one wasted round trip, and often several, because the only way to find the supported call is to guess again.

Webcmd already ships one error that gets this right — the fix is to make the rest match it.

What agents actually do

From an internal agent-behaviour eval:

Error returned What the agent did next
Page.close is unavailable in browser run. Abandoned the required new-tab workflow entirely and navigated the current page to a hardcoded URL
'DataTransfer' is not defined Guessed Buffer
'Buffer' is not defined Guessed TextEncoder
'TextEncoder' is not defined Gave up on the page, reimplemented the task by hand on about:blank

Contrast with the one message that already names an alternative:

BROWSER_RUN_API_UNSUPPORTED: File paths are unavailable in the QuickJS sandbox;
use in-memory file payloads

That agent stopped guessing about file paths immediately and moved to payloads. It is the same agent, the same run, the same sandbox — the only difference is that the message pointed somewhere.

Why they did that

An agent treats an error as the sole authority on what to do next. When the message is X is unavailable, the entire usable content is a negation: it eliminates one candidate from an unbounded space and offers no gradient. The rational next move is to try the next-most-plausible API, which is exactly the guess-chain above.

This is why the failures cluster rather than terminate. One dead end produces one more guess, and each guess costs a round trip against a live browser. Agent A's four-step chain in #335 is a single missing sentence repeated four times.

Root cause

src/browser/run/runner.ts:392-395 — a second, separate thrower with the same message shape. Note it serves only two literal APIs (Host filesystem reads, BrowserType.connect), neither of which needs a remediation today, so routing it through the map changes no message:

const unsupported = api => {
  const error = new Error(
    'BROWSER_RUN_API_UNSUPPORTED: ' + api + ' is unavailable in browser run.'
  );

Same shape at src/browser/run/playwright-transport.ts:439.

The template has no slot for a replacement, so no call site can supply one even where the replacement is obvious and stable.

Fix

Add a remediation slot to the thrower and populate it from a small static map, defaulting to the current text when an API is not in the map:

The map should be keyed by Type.method and cover exactly what DENIED_METHODS
(src/browser/run/playwright-transport.ts:55) actually blocks:

const REPLACEMENT = {
  'Page.close':                     'the session owns its tabs; leave it open, or end the session with `webcmd session close <session-id>`',
  'Browser.close':                  'end the session with `webcmd session close <session-id>`',
  'BrowserContext.close':           'end the session with `webcmd session close <session-id>`',
  'Browser.newContext':             'a run is scoped to one context; start another with `webcmd session create`',
  'Browser.newContextForReuse':     'a run is scoped to one context; start another with `webcmd session create`',
  'Browser.newBrowserCDPSession':   'raw CDP is not exposed inside browser run',
  'BrowserContext.newCDPSession':   'raw CDP is not exposed inside browser run',
  'Playwright.newRequest':          'use `page.request` for HTTP calls in the page context',
};

Correction: Page.$, Page.$$, and context.newPage() are NOT blocked

An earlier revision of this issue proposed mapping Page.$page.locator(selector) and
BrowserContext.newPage → "create tabs through Webcmd commands". Both were wrong, and
shipping them would have documented restrictions that do not exist:

  • Page.$ and Page.$$ are absent from DENIED_METHODS, and the generated client ships
    them. They work. (page.locator() is still preferable for auto-waiting, but that is
    advice, not a restriction.)
  • context.newPage() works too — playwright-transport.ts:206 routes newPage to
    scope.createPage, creating a session-tracked tab. What actually fails is closing it,
    because Page.close is blocked.

Only APIs present in DENIED_METHODS belong in this map.

For undefined globals rather than blocked Playwright methods (Buffer, TextEncoder, DataTransfer), the same treatment belongs at the QuickJS boundary: catch the is not defined rejection, and when the missing name is one Webcmd has an equivalent for, append it — Buffer is not available; use Uint8Array with TextEncoder/TextDecoder. That depends on #335 landing first, and should be a separate change: the is not defined path lives in runner.ts's QuickJS shim, which #335 rewrites, so bundling the two guarantees a conflict.

Check

One test per mapped API asserting the returned message contains the replacement, and one asserting an unmapped API still produces the current generic text.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions