dánwòdánwò

Core concepts

Replay cache

Agent steps are recorded the first time they pass and replayed after that, with no model calls.

The replay cache keeps a recording of every agent step that passed. The next run replays that recording instead of asking the model, so reruns are fast, free and give the same answer every time.

First runno recording yetModel actsand is checkedRecording keptin cache.dirNext runrecording foundReplayed0 model callsPage changed?model takes overrecords it afresh
The model runs once per step. After that, the recording replays it.

How a step is recorded

The first time an agent.act passes, danwo keeps what the model did. It keeps each element it acted on, described by what it is (its role, name, label and text), not by its position. It also keeps what the step changed, on the page and behind it.

On the next run, danwo finds each element again by that description and repeats the actions.

A new recording is kept only once a check after it passes, such as an expect on the page it led to. If the test fails before any check confirms the step, the recording is dropped, so a step that went wrong never becomes the recording.

The changes it keeps leave out text that differs on every run, so a replay doesn't wait for it:

Left outFor example
Times, dates, relative times3pm, Oct 20, 7 sec. ago
Durations321ms, Took 1.2s
Result counts and rangesShowing 1 to 9 of 9 results, 3 records imported
Badges that are only a number11, 99+
Route announcersNext.js reading out the new page's title

The page a step ends on is kept with its ids taken out, so /projects/PROJ-016 and /projects/PROJ-017 count as the same page. A unique() value in the path, such as a new company's name, is filled in with this run's value.

The other agent steps are recorded too:

StepWhat's recordedWhat the replay does
agent.assertA short text on the page that shows the claim holdsWaits for that text to be shown
agent.waitForThe same, for the conditionWaits for that text to be shown
agent.extractThe elements each value was read fromReads them again and checks the schema

A count with no text that states it, such as the number of cards on a board, is recorded as the kind of element counted. The replay counts them again.

Steps that mean a position

A replay finds an element by what it is, not where it sits. So "open the first invoice" is recorded as "open Invoice 42", and still opens Invoice 42 after a newer invoice moves to the top.

When the position is the point, find it with a locator and leave the rest to the agent:

tests/invoices.danwo.ts · a position
await screen.getByRole("row").nth(1).click()
await agent.act("download the invoice as a PDF")

When the item is the point, name it in params, as in "open the invoice {number}". Either way, the test says what it means, and the replay can't quietly pick another row.

Read the cache line

Every run with agent steps prints a cache: line, counting what each step came to:

cache: 12 replayed · 1 relocated · 1 handed off · 0 missed
OutcomeWhat happened
replayedThe recording replayed as recorded, with no model.
relocatedIt replayed, but an element was found somewhere other than recorded.
handed-offThe replay stopped matching, so the model took the step over.
missedThere was no recording, so the model ran.
staleA recording of this step exists under another key, so it wasn't used.
divergedUnder strict replay, the recording no longer replays.
offThe session has no cache.

A handed-off step is recorded afresh once it passes, so the next run replays again.

When steps made new recordings, the line also says what became of them:

cache: 3 replayed · 0 relocated · 0 handed off · 2 missed · 1 recorded · 1 not kept: the test failed
NoteWhat it means
recordedThe new recording was kept. The next run replays it.
not kept: the test failedA step failed before any check confirmed this one.
not kept: no check after it passedNothing confirmed the step worked. Add a check after it.

A step that wasn't kept asks the model again on the next run.

When you change the model

Each recording keeps the name of the model that made it. Changing model in your config doesn't throw recordings away: steps still replay with no model call. The cache line says the new model didn't run for them:

cache: 4 replayed · 0 relocated · 0 handed off · 0 missed · recorded with openai/gpt-5-mini (4 steps), running anthropic/claude-sonnet-5

To try the new model on those steps, clear the cache with danwo cache clear and run again.

Why a step was re-asked

A step is handed back to the model only when its recording stopped matching the page. The failure page and the report say what stopped matching, next to the outcome:

| 3 | assign Review Q3 plan to Bob | passed | handed-off: action 4 (click): getByRole("option", name: "Bob Babbage") is no longer on the page |
| 4 | Bob is assigned to Review Q3 plan | passed | handed-off: "Assigned: Bob Babbage" was not shown within 5000 ms |
ReasonWhat changed
… is no longer on the pageThe element was renamed or removed.
… matches 2 elementsThe description now fits more than one element.
effect: expected …, saw noneEvery action ran, but the change it made last time didn't happen.
"…" was not shown within … msThe text that proved a claim or a wait isn't on the page any more.

A reason that names a real change in your app means the replay caught it. The model's new run is recorded once it passes. A change on the page is waited for up to the action timeout, so a page slowed by load still gets there. A provider call that background work makes, such as a queued job sending a notification, is waited for up to 10 seconds before it counts as missing. A recording waits for the backend to go still before it keeps a step's calls, so a call the step set off that lands just after it (an email sent once a model's answer was saved) is kept too, and a quicker replay that makes it within the step still matches.

When a recording goes stale

A step's recording is keyed by its instruction and its options. Change the instruction, its params names, or the page it starts on, and the old recording no longer applies:

cache: 3 replayed · 0 relocated · 0 handed off · 0 missed · 1 stale

Rerun the test to record the new version. The failure page names what changed.

The step's position in the test isn't part of the key, so adding a step before it changes nothing. The same instruction twice in one test, such as two click Continue steps, is recorded twice, once for each time it appears. A test that runs the same steps as different users should name the user in params, as in { persona: "admin" }.

Replay in CI

Commit the cache folder, set by cache.dir in your config, so CI replays with no model calls:

danwo.config.ts · the cache
cache: { dir: ".danwo-cache" },

In CI the cache is read-only: steps replay from it, but nothing new is written, so CI leaves the cache as you committed it. A step that misses there calls the model on every run. Rerun it locally, check it, and commit the new recording.

To fail such a step instead of calling the model, run danwo test --replay-only. A step with no recording that replays then fails with REPLAY_MISSING, and the run makes no model calls.

A claim with no recorded proof, such as one that something is absent, fails too. See CI and GitHub to turn it on in the config.

Manage recordings

npx @danwo/cli cache ls
npx @danwo/cli cache stats
npx @danwo/cli cache clear

ls lists the recordings, stats counts them by outcome, and clear deletes them so every step records afresh.

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI