diff --git a/backend/apps/agents/browser/browser_agent.py b/backend/apps/agents/browser/browser_agent.py index 45bf3eb8..1be0462b 100644 --- a/backend/apps/agents/browser/browser_agent.py +++ b/backend/apps/agents/browser/browser_agent.py @@ -298,6 +298,9 @@ async def execute_browser_tool( params["selfheal"] = os.environ.get("OSW_SELFHEAL_CLICK", "1") != "0" if os.environ.get("OSW_CLICK_EFFECT_PROBE") == "1": params["effectProbe"] = True + # Document-order interactives display (default on); OSW_DOC_ORDER=0 = legacy rank-order, for the A/B off-arm. + if action == "list_interactives": + params["docOrder"] = os.environ.get("OSW_DOC_ORDER", "1") != "0" request_id = uuid4().hex result = await ws_manager.send_browser_command( request_id, action, browser_id, params, tab_id=tab_id, diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index bb7c26ed..235f61c8 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -883,7 +883,7 @@ async function handleListInteractives(wv: BrowserWebview, params: Record x.backendNodeId), [1, 2, 3]); assert.ok(shown.some((x) => x.role === 'weirdrole')); }); + +test('docOrder:false keeps legacy rank-order display (the A/B off-arm)', () => { + const { shown } = rankAndCapInteractives([ + mk('option', 'opt', 1), + mk('button', 'Go', 2), + mk('textbox', 'email', 3), + ], { docOrder: false }); + // rank order: textbox(0) < button(1) < option(3) + assert.deepEqual(shown.map((x) => x.backendNodeId), [3, 2, 1]); +}); diff --git a/frontend/src/shared/interactiveRanking.ts b/frontend/src/shared/interactiveRanking.ts index db6432de..a104c2fd 100644 --- a/frontend/src/shared/interactiveRanking.ts +++ b/frontend/src/shared/interactiveRanking.ts @@ -48,6 +48,9 @@ export interface RankOptions { cap?: number; // The agent's current goal; elements whose name matches it float to the top so the thing the model is actually looking for survives the cap. goal?: string; + // Display the surviving items in document order (default). false = legacy rank-order + // display, kept only so the A/B can measure the document-order win against it. + docOrder?: boolean; } // Words too generic to be useful signal, including the browser-action verbs and UI nouns that would otherwise match half the page ("click the button"). @@ -93,6 +96,7 @@ export function rankAndCapInteractives( // ordinals ("the 4th story" landed at [48], out of order), forcing the model to // burn a turn reading page text just to recover position; document order lets it // count directly. The high-signal-subset win (from the cap) is untouched. - const shown = selected.sort((a, b) => a.i - b.i).map((x) => x.it); + const displayed = opts.docOrder === false ? selected : selected.slice().sort((a, b) => a.i - b.i); + const shown = displayed.map((x) => x.it); return { shown, truncated: Math.max(0, ranked.length - shown.length) }; }