A3Node
DOMLoader drives a browser page to a stable state before Yosoi captures HTML. It clears common blockers, clicks content triggers, scrolls when needed, and waits for the DOM to settle.
A3Node caches that work. After a successful probe, Yosoi stores the page-stability recipe for the domain. On the next browser fetch, it replays the recipe first and avoids the full behavior-tree search when replay works.
What Gets Stored
An A3Node file stores the domain, an ordered list of acts, replay counters, and timestamps.
{ "format": 2, "domain": "shop.example.com", "acts": [ { "kind": "load_more", "cycles": 7, "target": { "type": "role", "value": "button", "name": "Load more", "nth": 0 } } ], "discovered_at": "2026-05-23T14:00:00Z", "replay_count": 4, "last_replayed_at": "2026-05-23T18:00:00Z"}An empty acts list is valid. It means the domain was probed and needed no DOM actions. Yosoi stores that result so the next run can skip the probe and capture the page after its settle window.
Storage
Recipes live under .yosoi/a3nodes/.
.yosoi/ a3nodes/ a3node_shop_example_com.jsonTreat this as local operational state. It is useful to share inside a controlled scraping workflow, but it is not a public contract format.
Enabling A3Node
A3Node is opt-in.
from yosoi.core.fetcher import create_fetcher
async with create_fetcher("waterfall", experimental_a3node=True) as fetcher: result = await fetcher.fetch("https://shop.example.com/catalog")You can also enable it on browser fetchers directly:
from yosoi.core.fetcher.waterfall import JSFetcher
async with JSFetcher(experimental_a3node=True) as fetcher: result = await fetcher.fetch("https://shop.example.com/catalog")Without experimental_a3node=True, browser fetchers run a fresh DOMLoader probe.
Replay Behavior
On each browser fetch:
- Yosoi loads the domain recipe from
.yosoi/a3nodes/. - Empty recipes skip DOM actions and capture content after settling.
- Non-empty recipes replay stored acts through
DOMLoader.replay. - Successful replay increments
replay_count. - Failed or insufficient replay falls back to a fresh
DOMLoaderprobe. - A successful probe saves the new recipe.
Replay avoids the full trigger search on the hot path when stored targets are available. Older format recipes without stored targets can still fall back to probing for that act kind.
Boundary With MCP Lessons
A3Node is fetch-time stabilization. It is not the MCP lesson store.
| System | Job | Storage |
|---|---|---|
| A3Node | Get the page into a stable DOM state before HTML capture. | .yosoi/a3nodes/ |
| DiscoveryLesson / ReplayPlan | Preserve MCP-learned browser discovery behavior. | .yosoi/lessons/ |
Keep those boundaries separate. A3Node answers “how do I make this page loaded enough to scrape?” MCP lessons answer “what did the agent learn while discovering this contract?”
Inspecting Recipes
from yosoi.storage.a3node import A3NodeStorage
storage = A3NodeStorage()node = await storage.load("shop.example.com")
if node: print(node.acts) print(node.replay_count) print(node.battle_tested)To force a fresh probe:
await A3NodeStorage().delete("shop.example.com")Or remove the local recipe file from .yosoi/a3nodes/.
FAQs
Is A3Node enabled by default?
No. It is experimental and opt-in through experimental_a3node=True.
What does A3Node store?
It stores the ordered DOMLoader actions that made a domain stable, plus replay counters and timestamps. Empty recipes are stored too.
Does A3Node replay non-empty recipes?
Yes. Current main replays stored acts through DOMLoader.replay and falls back to a fresh probe when replay falls short.
Is this the same as MCP DiscoveryLesson replay?
No. A3Node stabilizes pages during fetch. MCP lessons are discovery artifacts.
When should I delete an A3Node recipe?
Delete it when a site redesigns, replay falls back repeatedly, or you want DOMLoader to rediscover the page-stability recipe.