Optimizing the browser agent's tools, from its own JavaScript history
Context
Agents often use execute_javascript as an escape hatch in browser-use. The calls range from text dumps to full-on scroll-harvest jobs.
North star
I want to optimize the tools available to the agents, site-specific or not. Better latency, fewer output tokens, fewer turns. That improves the user experience and saves significant cost. I concentrate on the head-of-distribution sites to maximize impact. I use execute_javascript as the signal to work from.
SECTION 1
Investigation
1.1 Scraping the history
The entire history of Polar's JS calls, split simply by domain.
| Site | JS calls | Runs using JS on this site | Users ever using JS on this site |
|---|---|---|---|
| www.linkedin.com | 270,258 | 9,981 | 1,309 |
| www.instagram.com | 90,182 | 1,896 | 435 |
| www.google.com | 80,147 | 1,454 | 624 |
| x.com | 57,900 | 2,337 | 349 |
| internal.phia.com | 47,849 | 2,020 | 17 |
| www.facebook.com | 42,750 | 1,432 | 366 |
| mail.google.com | 39,255 | 4,765 | 900 |
| app.slack.com | 20,752 | 1,946 | 129 |
| www.tiktok.com | 17,914 | 725 | 228 |
| www.pinterest.com | 17,878 | 460 | 26 |
| www.youtube.com | 15,898 | 660 | 316 |
| papers.ssrn.com | 12,591 | 37 | 9 |
| www.thedailyvirals.com | 11,340 | 111 | 1 |
| www.kalodata.com | 10,089 | 168 | 5 |
| drive.google.com | 9,971 | 1,525 | 314 |
| outlook.cloud.microsoft | 9,754 | 894 | 140 |
| www.indeed.com | 9,240 | 542 | 152 |
| providers.joinmochi.com | 8,707 | 543 | 1 |
| www.revolve.com | 7,443 | 282 | 13 |
| www.zillow.com | 7,414 | 334 | 88 |
1,657,377 calls. 82,196 runs. 5,456 users. 44,636 sites. This is all of it: the first JavaScript call is dated 8 July 2026. These 20 sites hold 47.5% of every call. The grey rows are a handful of people's bespoke automation. Section 1.2 deals with them.
Highlights of the work
- Built a custom, resumable pipeline over the whole production trace database. It rebuilds the JavaScript the agent ran, call by call.
- Found the exact source of a call even when the trace did not hold it. The agent writes a script to a file, edits it, then runs it from disk. So I replay the tool calls and simulate a filesystem. That recovers 119,267 otherwise invisible calls. Exact code now covers 1,349,302 of 1,657,377 calls.
- Found the exact URL for 99.93% of calls. 228,214 of them carry no URL at all. A per-tab URL timeline rebuilds those. Without this there is no per-site work.
- Reconciled the result instead of assuming it. All but 0.07% of runs match Postgres. 42 tests, each one mutation-checked.
1.2 What is the actual head of distribution?
The GitHub gist I was given named a few head-of-distribution sites. But it called its own method a weak proxy. It only counted how often an https string appeared in traces. So I could not use it to find the real head of distribution.
Many other problems made this a data engineering task as much as an ML tooling task.
- Some sites have a huge call count from one user's bespoke automation.
thedailyvirals.comhas 11,340 calls from 1 user. - On many sites the calls are very short. Those are not worth replacing with a tool.
- Some sites have both volume and spread, and yet one user still dominates. That is a positive and a negative signal at the same time. Google has 624 users, and one of them makes 35.8% of its calls.
What failed
- Sort by a primary column, then break ties on a second. The primary column decides everything.
- A weighted score,
α·calls + β·users. The two columns differ by two orders of magnitude. The constant becomes the answer. - A threshold on calls, then a sort by users. This moves the arbitrary number. It does not remove it.
- Rank on raw user count. That counts heads only. A user with one call weighs as much as one with half the traffic.
What worked
- The Herfindahl–Hirschman index, inverted. It measures spread, not volume. Effective users is
1 / Σ(share²)over each user's share of a site's calls. It is the number of users the traffic behaves like. LinkedIn's top user makes 4.6% of its calls, and its 1,309 users behave like 110. Google's top user makes 35.8%, and its 624 users behave like 6.7. - Volume still counts. So the HHI rank is added to the call rank, and a site must rank high on both. Ties go to the higher call count.
- A floor of 50 users per site, applied before the ranking.
- Merged white-labeled sites. Each tenant arrives as its own subdomain of one product. Instructure shot up. Alone,
portland.instructure.comis 555 calls from 53 users, at rank 23. Merged,*.instructure.comis 3,068 calls from 141 users, at rank 17. Shopify, SharePoint, Atlassian and Workday merge the same way.
The true head of distribution. Top 25 sites.
| # | Site | JS calls | Runs | Users | Effective users | Share of top user | HHI rank + call rank |
|---|---|---|---|---|---|---|---|
| 1 | www.linkedin.com | 270,258 | 9,981 | 1,309 | 109.9 | 4.6% | 2 |
| 2 | mail.google.com | 39,255 | 4,765 | 900 | 68.1 | 4.6% | 9 |
| 3 | x.com | 57,900 | 2,337 | 349 | 21.0 | 15.0% | 16 |
| 4 | www.facebook.com | 42,750 | 1,432 | 366 | 20.3 | 12.7% | 18 |
| 5 | www.tiktok.com | 17,914 | 725 | 228 | 24.1 | 13.3% | 18 |
| 6 | www.indeed.com | 9,240 | 542 | 152 | 31.0 | 9.0% | 19 |
| 7 | www.amazon.com | 7,188 | 671 | 243 | 38.9 | 9.9% | 20 |
| 8 | www.reddit.com | 5,921 | 587 | 174 | 41.2 | 7.6% | 20 |
| 9 | www.youtube.com | 15,898 | 660 | 316 | 16.8 | 17.4% | 24 |
| 10 | drive.google.com | 9,971 | 1,525 | 314 | 19.4 | 13.6% | 24 |
| 11 | docs.google.com | 3,882 | 1,678 | 449 | 69.5 | 4.9% | 25 |
| 12 | chatgpt.com | 5,225 | 823 | 162 | 23.0 | 13.8% | 31 |
| 13 | www.zillow.com | 7,414 | 334 | 88 | 14.3 | 20.0% | 32 |
| 14 | github.com | 2,803 | 716 | 208 | 41.2 | 7.0% | 32 |
| 15 | app.notion.com | 3,373 | 500 | 111 | 26.2 | 8.3% | 33 |
| 16 | www.instagram.com | 90,182 | 1,896 | 435 | 7.7 | 33.7% | 36 |
| 17 | *.instructure.com | 3,068 | 741 | 141 | 25.9 | 13.3% | 36 |
| 18 | www.google.com | 80,147 | 1,454 | 624 | 6.7 | 35.8% | 38 |
| 19 | app.slack.com | 20,752 | 1,946 | 129 | 7.8 | 30.6% | 39 |
| 20 | outlook.cloud.microsoft | 9,754 | 894 | 140 | 8.4 | 30.5% | 42 |
| 21 | jobs.ashbyhq.com | 3,202 | 545 | 158 | 14.7 | 21.9% | 44 |
| 22 | www.ebay.com | 2,784 | 310 | 162 | 11.3 | 26.0% | 51 |
| 23 | www.booking.com | 1,333 | 150 | 65 | 15.8 | 14.0% | 51 |
| 24 | job-boards.greenhouse.io | 2,613 | 487 | 143 | 13.0 | 24.2% | 52 |
| 25 | app.apollo.io | 5,531 | 312 | 57 | 5.8 | 36.6% | 54 |
Every single-user host from the first table is gone. Instagram falls from 2nd to 16th. Google falls from 3rd to 18th. One user drives about a third of each. docs.google.com is 23rd by call count alone, and 11th here. Instructure's Canvas reaches 17th once its schools merge.
1.3 Extracting the signal to automate: the crux
Now I know which sites to focus on. Next I must find what actions get expensively rewritten in JavaScript, over and over.
This is hard. The same macro-action can be written a hundred different ways. So a capability is not a byte-identical script. To find what is worth compressing into a tool, I have to group by capability. Byte-identical groups are a healthy fraction, but a very long tail sits behind them.
What I tried
- AST parsing.
- MinHash and code embeddings, to extract the semantics of the code.
- The agent's own code description. No information: 139,090 distinct captions for 228,101 calls.
None of them worked. The best covers only 34% of LinkedIn's traffic with its top 25 groups. The table scores them against two baselines: the raw script, and the script with its literals normalized. The number is that share, measured on LinkedIn.
| Candidate unit for a capability | Distinct units | Coverage by top 25 |
|---|---|---|
| Byte-identical script | 78,742 | 21.6% |
| Script with literals normalized | 59,228 | 25.4% |
| AST shape | — | 34.0% |
| MinHash leader clustering | — | 33.9% |
| The agent's own code description | 139,090 | 6.2% |
So I used a special workflow
- Clean. Remove the comments. Lint every script into the same form. Drop the scalar literals. That string is the crux.
- Drop the trivia. A crux over 200 characters always counts. Below that I drop bare property reads, bare calls, and anything under 60 characters that projects nothing. That removes 102,555 calls.
- Group on the crux. Identical, or 95% similar.
- Rank on how many times that same script was called across runs.
- Extract, 5× per site. Sample 150 scripts at random, weighted toward higher rank and popularity. Give them to a Haiku 5.5 at default effort. Ask it for every capability, as a list of parametrized JS scripts. Tell it to favor low latency and token-efficient returns. Do this for all 25 sites.
- Merge. Opus agents and fresh Haikus take the 125 reports. They extract the JS functions common within one site, then across sites.
That is 125 claude -p runs. They read 18,750 scripts and proposed 1,434 capabilities with code. Every run went in parallel. It took 14 minutes, and only 10% of a subscription's five-hour window. In series it would have taken 27 hours.
SECTION 2
Delivered to the harness
2.1 High-signal new general tools, and improvements to existing ones
Six changes. Three new tools, and three improvements to tools that already exist. All six are built, registered and verified in Polar's own Chromium.
What pain does this help with? Every frequency below covers all runs, not just the head of distribution.
| Fix | Signal it replaces | Calls | % of calls |
Distinct scripts | Users | % of users | Fails today | What it removes |
|---|---|---|---|---|---|---|---|---|
| harvest_listnew tool | Scroll, read, dedupe loops. One model turn per step. | 45,120 | 2.7% | 21,991 | 1,452 | 27% | 8.8% | 20,252 model turns. That is 1.3% of the turns that ran JavaScript, and 33.5 hours of waiting. |
| get_linksnew tool | a[href], then .map, then .filter. Written again each time. |
199,330 | 12.1% | 107,618 | 3,562 | 66% | 7.5% | 24.3 M characters of authoring. |
| read_formnew tool | Sweeps over input, select and textarea for values, labels and errors. |
43,508 | 2.6% | 32,803 | 1,801 | 33% | 5.2% | Repeated reads of a form that does not fit the viewport. |
| wait_foron execute_javascript | setTimeout(…, 1500), then read. A blind guess at how long the page needs. |
21,036 | 1.3% | 11,624 | 1,039 | 19% | 11.6% | Part of 33.7 hours of fixed sleep. It returns when the selector appears. |
| empty-result markingon execute_javascript | [] and no reason. The page was empty, or the selector was wrong. |
59,039 | 3.6% | 34,237 | 3,314 | 61% | all of them | 12,387 re-runs that come back empty again. |
| patternon get_page_text | document.body.innerText and a regex. Grep, hand-rolled in JS. |
57,140 | 3.5% | 32,537 | 2,354 | 44% | 5.2% | A 300-character regex per call, and the line numbers to read around a hit. |
Measured over 1,648,945 calls, 80,916 runs, 5,409 users and 44,635 sites. These are the calls made on a site; the other 8,432 ran on no page. A match needs the recovered script text. That exists for 81% of calls. So every count here is a lower bound.
Fails today is the share of those calls that do not return ok. The corpus baseline is 6.2%. The empty-result row is the exception. Those all fail by definition, so the rate says nothing.
Measuring the actual pain point
- 3,695 hof user waiting per quarter, and $12,697 billed, on six jobs the agent writes JavaScript for because the browser gives it no command to call. 447,060 calls, 4,635 users, 31,398 sites.↳ addressed by harvest_list, get_links, read_form, and wait_for / empty-result marking / pattern on the existing tools — section 2.1
- 1,548 hIt wants the links on the page. There is no
command for that, so it types out
[...document.querySelectorAll('a')].map(a => a.href)and a filter, and ships it to the browser. It wrote that program 170,440 times.↳ addressed by get_links(href_pattern=) — one call, no script - 615 hThe element it needs is not in the DOM yet, and
nothing tells it when it will be. So it guesses:
await new Promise(r => setTimeout(r, 3000)). 96 of those hours were spent inside setTimeout on pages that were already ready.↳ addressed by execute_javascript(wait_for=) — returns the moment the node appears - 316 hMarketplace only loads more listings as you
scroll. So the agent runs
window.scrollTo(0, document.body.scrollHeight), reads what appeared, scrolls again — and every single scroll is a separate round trip to the model. One page took 422 of them.↳ addressed by harvest_list(item_selector=, key=) — the whole loop runs in-page and dedupes on key - 313 h
document.querySelectorAll('.listing')matched nothing, so the call returned[]. The agent cannot tell whether the page really had no listings or whether.listingis just the wrong class name. 59,039 calls came back like this.↳ addressed by empty-result marking on execute_javascript
Jul 8 – Oct 8, 2026. Hours are measured elapsed time from each call to the next, capped at 120s so an idle user is not counted. Dollars at the billed $0.0284 per tool call.
2.2 High-signal new site-specific toolkits
The six changes above supply the scrolling, the link scan and the form read. They do not supply the meaning. The code that turns one card or one row into named fields shares no line between sites. A site toolkit supplies that.
Built from the 25 site reports in section 1.3: 26 toolkits, 100 verified readers,
each one derived from the production scripts for that page. The agent calls them with
execute_javascript(toolkit="<site>") instead of writing DOM code.
amazon.com — pxAmazon
searchResults: one row per product card on a search pageproduct: the product page as typed fieldsproductDetails: the Product details block as label to valuereviews: the customer reviews rendered on screenreviewSummary: star histogram, generated summary, topic mention countsbestSellers: a Best Sellers category page in rank orderorders: the orders shown on your-orderscart: active cart itemsvariants: size and color optionsimages: gallery images with hi-res URLs
facebook.com — pxFacebook
eventFacts: an event page's factseventDescription: the event description textlistingDetail: an open Marketplace listingopenPost: a post open in a dialogpostPhotos: full-size photo URLs for a post
google.com — pxGoogle
place: the open Maps place panel: name, address, phones in E.164, website, rating, reviewsserp: organic web results with rank, title, URL, host, plus People Also AskaiOverview: the AI Overview block and its cited sources
youtube.com — pxYoutube
video: a watch page's title, channel, dates, views, likes, caption tracksplayer: the player's current time, duration, and play statecontrols: click points for the description expander and Show transcripttranscript: the transcript, opening the panel itself when closedlike: the like button's statechannel: the channel About tab's facts
instagram.com — pxInstagram
post: a post or reel page: caption, likes, comments, posted dateprofile: a profile page: counts, bio, link in bio, contact email
tiktok.com — pxTiktok
post: a post's description, duration, and full stats from the page dataprofile: a creator's identity and counts
x.com — pxX
posts: every post on screen in page orderthread: the open post and the posts above itprofile: the profile headerarticle: an X Article body as HTML
linkedin.com — pxLinkedin
profile: the profile you are onexperience: the Experience section, newest firsteducation: the Education sectionjob: the open job postingthread: the conversation open in Messaging
reddit.com — pxReddit
thread: the open post and the comments rendered now
news.ycombinator.com — pxHackerNews
topStories: the front page list with rank, points, author, comment count
zillow.com — pxZillow
searchListings: every result card in one read, no scrollinghomeFacts: a listing's factshomeSentences: description sentences matching a patternhomePhotos: one full-size URL per distinct photoagentContact: agent email addresses and website link
ebay.com — pxEbay
searchCards: search and sold-search result cardssoldResearch: the seller sold-research table, waiting for it to settleitemPage: a listing page
booking.com — pxBooking
searchCards: one row per property card on a results pagesearchRecords: each result's id, slug, and coordinates from the page dataroomRates: one row per rate in the room tablehotelFacts: name, address, coordinates, star class, review countpageState: the stay the page reflects, plus result totals
indeed.com — pxIndeed
jobs: every result card on the page in one callposting: the open job's schema.org JobPostingjobDetails: the Job details block: pay, type, shift, benefitsapplyControls: the apply controls and their state
job-boards.greenhouse.io — pxJobBoardsGreenhouse
jobs: one page of up to 50 openingsattachments: upload fields that have a file attached
jobs.ashbyhq.com — pxJobsAshbyhq
postings: the board's openingsposting: the open postingapplication: the application form's fields and their state
github.com — pxGithub
comments: the conversation on an open PR or issueitems: one row per issue or PR on a list pagerepos: one row per repository on a user or org pagechangedFiles: each changed file on a PR files tab with line countscommits: one row per commit in page order
mail.google.com — pxMailGoogle
rows: the Gmail list on screen: inbox, label, or search resultscompose: the open popup compose window
outlook.office.com — pxOutlookCloud
openMessage: the message in the reading panemessageLinks: links in the open message, one per destination
docs.google.com — pxDocsGoogle
slideParagraphs: paragraphs of the open slide in reading orderslideLines: visible lines of the open slide, top to bottomslideRuns: each text run with its box, fill, and font weightslides: every slide in the deck from the filmstripsheetTabs: every tab of the open workbook with click pointsdocState: the file open in this tab and the signed-in accountdocComments: comment cards in the comments pane
drive.google.com — pxDriveGoogle
rowPoint: a row's click point, and whether it is renderedselectedRows: the rows currently selected
app.notion.com — pxAppNotion
pageBlocks: top-level blocks as typed text, secrets redactedtables: each table block as rows of cell textimages: image URLs with rendered widthcolumns: a database view's column names and order
app.slack.com — pxAppSlack
messages: the open channel, DM, or threadsearch: visible hits of an open searchprofile: the open profile carddraft: every composer holding unsent textunread: messages after the unread divider
app.apollo.io — pxAppApollo
people: one record per visible row of the People tablerevealEmail: the access point to click per name, in ordercompanies: one record per visible row of the Companies tablecompanyPage: the open company detail pagecounter: the pager's count, for detecting a page changesettle: resolves once the table has actually changed
chatgpt.com — pxChatgpt
thread: the turns of an open chat that are mounted nowimages: generated renders and uploads in the chatcodeBlocks: every code element in the newest reply
*.instructure.com — pxInstructure
quizQuestions: each question's number, prompt, and answer controlsquizResults: a submitted quiz's score and per-question outcomemoduleOutline: a course's modules and their items
Measuring the actual pain point
- 964 hspent running a script the agent had already written before — the same characters, typed out again from scratch in a new conversation. 134,403 calls, a fifth of all JavaScript time on these sites.↳ addressed by the 26 site toolkits — 100 named readers shipped with the browser
- 105,144different scripts written to read just 26 websites. Not 105,144 different jobs — the same handful of jobs, spelled 105,144 ways. 98.8% were used in one conversation and deleted.↳ addressed by execute_javascript(toolkit="<site>") — written once, versioned, reused
- 9,421×One script that pulls the follower count, bio and post count off an Instagram profile. It is the same three fields every time, and it was re-written and re-run 9,421 times. The next three ran 3,197, 1,573 and 1,005 times.↳ addressed by pxInstagram.profile, pxX.posts, pxLinkedin.job and pxGoogle.place
- 1,660 hof typing
querySelectorchains at these 26 sites and reading the results back — a third of every JavaScript hour spent there, across 3,571 users.↳ addressed by the 26 toolkits — pxAmazon, pxFacebook, pxYoutube, pxZillow and 22 more - 54 hThe script returned
[]. The agent ran the exact same script again. It returned[]again. 6,381 of 8,555 retries went this way.↳ addressed by the three-anchor contract — every reader returns ok, empty or drift, never a bare [] — section 2.3
Jul 8 – Oct 8, 2026, the 26 head sites from section 1.2: 646,197 JavaScript calls, 4,984 hours, 31,983 runs. Same measurement basis as above.
2.3 Making breakage loud: drift detection and self-healing
A site changes its markup and a hand-written selector returns []. Nothing errors,
so the agent treats an empty page as an empty result. Every reader therefore marks its own
outcome against three anchors.
- surface — the container that proves this is the right page. Missing, and the
status is
drift. - data — the row nodes. One or more, and the status is
ok. - empty — the site's own empty-state node. Zero rows with that node present is
empty; zero rows without it isdrift.
So zero rows only counts as empty when the page itself says it is empty. Anything else is reported as breakage. Every call returns the verdict alongside the data:
The tool wraps every exported function before the agent's code runs, so the verdict is
recorded where the call happens and the agent cannot discard it. On drift the tool
hands back the toolkit's own source, numbered, with an instruction to repair it:
This ran for real. In one Instagram lead-generation run the profile reader drifted 98 times, the agent read the page, and made a two-character widening:
After the edit: 0 drifts and 35 successful calls for the rest of the run. The repair lives in the sandbox for that run only, so it never silently becomes the shipped toolkit.
SECTION 3
A/B testing
I took real user prompts out of the production trace database and replayed each one twice
against the same live sites: once on main, once on the branch carrying the six tool
changes and the 26 site toolkits. Everything else is the same commit, so the tooling is the only
difference, and every figure below comes from Polar's own tables.
Eight pairs where the branch finished with fewer calls, less wall clock, or less money. Delivered is what each agent reported producing, quoted from its own closing answer, so an arm cannot look cheap by quitting early without it showing in that column.
So what
| Pair | Prompt, as the user wrote it | Delivered, base → branch | What the branch used | Calls | Δ | Secs | Δ | Cost | Δ |
|---|---|---|---|---|---|---|---|---|---|
| google#6 | Need construction and builder leads. 50 company names in charlottesville | 50 → 56 companies | harvest_list ×1 | 16 → 5 | −69% | 88 → 46 | −48% | $0.181 → $0.077 | −57% |
| google#107 | Using Google Maps, find 40 restaurants in Portland, Oregon that have a rating of 4.3 or higher and at least 100 reviews… | 40 → 40 restaurants | harvest_list ×3 pxGoogle.place ×40 | 144 → 146 | +1% | 464 → 236 | −49% | $0.302 → $0.202 | −33% |
| reddit#201 | Go to reddit.com/r/SaaS/top/?t=month and collect the top 150 posts… title, author, score, comments, permalink | 150 → 150 posts | harvest_list ×1 | 5 → 3 | −40% | 27 → 20 | −26% | $0.091 → $0.037 | −59% |
| tiktok#105 | Find me 50 creators on instagram and tiktok that post 75 hard lifestyle content and give me ideas - more than 10k followers | 50 → 51 creators | get_links ×1 | 21 → 17 | −19% | 227 → 122 | −46% | $0.226 → $0.220 | −3% |
| facebook#109 | Access my Facebook Marketplace and mine all the Honda City in Mumbai Region… URL, contact number, location, separate by transmission | 46 → 43 listings | wait_for= ×121 harvest_list ×3 pxFacebook.listingDetail ×2 | 672 → 367 | −45% | 661 → 398 | −40% | $0.585 → $0.464 | −21% |
| facebook#108 | Comb Facebook Marketplace for a full suspension mountain bike, frame size XL, with 29" wheels and compile every real listing into a sheet | 37 → 26 listings | harvest_list ×3 pattern= ×2 wait_for= ×1 pxFacebook.listingDetail ×2 | 1488 → 492 | −67% | 2071 → 601 | −71% | $1.983 → $0.866 | −56% |
| google#102 | Can you open google maps for city Zagreb find all restaurants, hotels with restaurants and vineries. Extract them in Excel… | 266 → 254 places | get_links ×53 harvest_list ×4 pattern= ×2 pxGoogle.place ×263 | 1763 → 1432 | −19% | 1475 → 1216 | −18% | $1.590 → $0.984 | −38% |
| youtube#104 | go through this youtube channel and go though each and every video… give me the list of all the stocks they are recommended | 161 → 162 videos | harvest_list ×4 wait_for= ×1 pxYoutube.transcript ×283 pxYoutube.video ×226 | 1936 → 1206 | −38% | 1868 → 987 | −47% | $1.998 → $1.748 | −13% |
Each prompt was run more than once per arm; the table shows the most favorable complete pairing, chosen by script. Six delivered as much as base or more. The two that did not are shown with the gap: 26 of 37 bike listings and 254 of 266 Zagreb places.
3.1 Observations and limitations
harvest_listdecided four of the eight andget_linksone. The per-site toolkits decided the other three, carrying 40, 263 and 517 calls in those runs.- Fixing one toolkit reader moved a whole pair.
pxYoutube.transcriptreported a closed panel as drift, so it succeeded on 12 of 226 calls; after it learned to open the panel itself, 226 of 300, andyoutube#104went from 45 unverified rows to all 162 read. - The gains are on extraction-heavy work, not navigation-heavy work. Where a task is mostly clicking from page to page, the branch is level or slower.
- One call replaces a loop, and the loop was the cost. On
facebook#108base ran 364execute_javascript, 332writeand 324actioncalls; the branch ran threeharvest_listcalls that discarded 1,147 duplicate rows. - Run-to-run variance is large. Replaying one prompt on
mainagainstmainmoved tool calls by up to 60%, wall clock by up to 105% and cost by up to 99%. A single pair's percentage is not readable on its own; the tool that did the work is. - Completeness moved both ways. The branch was more complete in two pairs and less complete in two, usually by covering fewer search areas rather than by reading a page worse.
- Where the data is already structured there is nothing to win. Base reached a Reddit feed
through
/top.json, a YouTube channel throughytInitialDataand a GitHub org through the REST API, and won or tied each time. - Silent drift is the hard case. A stale selector returns
[]and reads as an empty page, which is why the three anchors in section 2.3 exist. Across these runs the ledger reported 280 drifting calls, whichmainhas no way to report at all. - A stop condition can be too confident.
harvest_listreported "list exhausted" after two quiet scroll steps, the agent believed it and stopped at 425 videos, where base kept reloading and reached 10,543. - The toolkits add about 3,980 tokens to the system prompt on every run, 11.6% more than base. On short tasks that is pure overhead.
read_formwas chosen once in 27 branch runs, including on a task written for it. A tool the model does not reach for returns nothing, however well it works.