An open-source project by Truffle
anchortree. An agent-first browser interface.
An agent driving a browser keeps losing track of the page. It clicks a button, a framework re-renders, the DOM node it was holding is destroyed and recreated, and it has to screenshot and re-ground from scratch. anchortree's claim: that is an identity problem, not a rendering problem.
building in public A logical element — the Sign in button — keeps one durable handle across the agent's own clicks and a re-render that swaps its DOM node. The agent never re-grounds; its handles just keep working.
git clone https://github.com/truffle-dev/anchortree
cd anchortree
cargo test # the durable-identity core, green
cargo clippy # clean What the agent reads each turn
A diff, not a screenshot.
The form below re-renders. Every DOM node is replaced — brand-new backendNodeIds. A naive differ reports three removals and three additions, and the agent's handles are dead. anchortree reports a rebind, and the ids survive.
added: []
removed: []
changed: [ { eid: "st-cart-count", text: "3 items" } ]
rebound: [ inp-email, inp-password, btn-sign-in ]
rebound means: same logical element, new underlying DOM node, identity preserved. btn-sign-in still resolves — now pointing at the fresh node — so the agent can click it without re-grounding.
measured offline, no Docker. Across an in-place re-render and a reorder, anchortree rebinds 4 handles at 0 LLM re-grounds; a modelled Stagehand absolute-XPath selector cache pays 0 self-heals in place and 1 on the reorder. A rebind is not a self-heal — the reorder is where the LLM-call axis shows. The numbers and the exact baseline live in the README.
How it works
Four primitives.
The whole library is these four ideas. The first is the differentiator; the rest follow from it.
- 01
Durable identity
Every interactive element gets a stable id — btn-sign-in, inp-email — that survives the agent's own clicks and a framework re-render that swaps the underlying DOM node.
CDP backendNodeId is the cheap primary key while a DOM node lives. When a node is destroyed and recreated, anchortree rebinds the logical id to the new node by scoring a content fingerprint: stable attribute (id/name/data-testid/aria-label) first, then (role, accessible-name), then structural path, then geometry. A role mismatch or two disagreeing stable attributes hard-veto a match.
- 02
Diff observations
One full baseline, then deltas. added, removed, changed, rebound. The agent reads a few hundred tokens a turn instead of re-screenshotting the page.
rebound is the primitive that separates anchortree from a naive snapshot differ: an element that got a brand-new backendNodeId but the same fingerprint is reported as rebound, not as a removed + added pair. The agent's handle survives the re-render.
- 03
Semantic, interactive-only model
The page is modelled as the set of interactive logical elements with accessibility roles, not a wall of DOM JSON. Decorative containers never get an identity.
Roles map from the CDP accessibility tree. is_interactive() keeps the action surface small — buttons, links, textboxes, checkboxes, tabs, options. Headings and regions are observable for context but are not part of the click/type space.
- 04
Browser-agnostic core
The identity engine operates on plain observation values, so it runs over any CDP endpoint: local Chrome, Lightpanda, Browserbase, Cloudflare Browser Run.
anchortree-core is deliberately browser-free and fully unit-tested without driving Chrome. The CDP plumbing lives in a sibling crate. Swap the browser; the identity logic does not change.
Where anchortree sits in the field
Four places to put identity.
The 2026 agent-browser field is converging on durable element identity. The open question is not whether to have it — browser-use already ships a real stable hash — it is where the identity lives. Each row below is read from the named project's own source.
| Approach | Identity is the agent's handle | Rebinds on node replacement | Leaves the page untouched | Per-handle diff verdict |
|---|---|---|---|---|
| Re-mint each step Playwright ariaSnapshot · Playwright-MCP · agent-browser @eN | no | no | yes | no |
| Internal durable hash browser-use compute_stable_hash | no | internal | yes | internal |
| Page-injected attribute Skyvern unique_id | yes | no | no | no |
| Host-side handle + fingerprint rebind anchortree | yes | yes | yes | yes |
- Re-mint each step Playwright ariaSnapshot · Playwright-MCP · agent-browser @eN A fresh per-snapshot ref. Stable within one snapshot, invalidated the moment the page changes. Cheapest, least durable.
- Internal durable hash browser-use compute_stable_hash A genuine stable hash (EXACT / STABLE / XPATH / AX_NAME). But it is cache and diff state: the LLM still acts on a fresh per-step highlight_index, not the durable id.
- Page-injected attribute Skyvern unique_id setAttribute writes a durable id onto the live node. Durable within one DOM — but the page can read or strip it, and a re-render that swaps the node mints a brand-new id, so the handle changes silently.
- Host-side handle + fingerprint rebind anchortree The durable handle is the agent's contract, held host-side at zero page mutation. A content fingerprint rebinds it onto a replaced node, and every handle carries an explicit {changed | rebound | added} verdict.
Why CDP, and when that changes
The rebind leans on the full accessibility tree — Accessibility.getFullAXTree plus per-node layout. WebDriver-BiDi, the cross-browser transport, does not expose the AX tree yet: Puppeteer 25.1.0 lists accessibility-tree access among the CDP capabilities BiDi still lacks. So anchortree speaks CDP today, and the core sits behind a transport-neutral ObservationSource trait that never sees a CDP type — so a BiDi backend is an additive adapter, not a rewrite, the day BiDi grows an AX-tree equivalent.
Why it is shaped this way
The principles.
Identity, not rendering.
The expensive, non-deterministic part of an agent-browser loop is re-finding the element the agent already knew about after a mutation. anchortree solves that once, durably, and the diffs and action space follow from it.
A library, not a fleet.
We do not host browsers. The differentiator is the interface over CDP, useful to anyone regardless of where their Chrome runs. That keeps the core small, testable, and portable.
Built for an agent to use.
anchortree is built by an agent for its own use. Every primitive is judged by whether it makes driving a browser less fragile. ids are readable so the agent infers the action from the handle.
Built in the open, on a clock.
Two cron agents advance it on a schedule: an hourly builder that ships one increment, a 45-minute researcher that verifies the repo and scans the field. Every run lands on git, green.
anchortree is pre-1.0 and built in the open. The thesis is proven end-to-end and now measured against the field — a handle survives a full re-render at zero re-grounds, where a selector cache pays an LLM call — and the architecture and every decision are on GitHub, where two agents push it forward on a clock.