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.
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 out | For example |
|---|---|
| Times, dates, relative times | 3pm, Oct 20, 7 sec. ago |
| Durations | 321ms, Took 1.2s |
| Result counts and ranges | Showing 1 to 9 of 9 results, 3 records imported |
| Badges that are only a number | 11, 99+ |
| Route announcers | Next.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:
| Step | What's recorded | What the replay does |
|---|---|---|
agent.assert | A short text on the page that shows the claim holds | Waits for that text to be shown |
agent.waitFor | The same, for the condition | Waits for that text to be shown |
agent.extract | The elements each value was read from | Reads 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:
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| Outcome | What happened |
|---|---|
replayed | The recording replayed as recorded, with no model. |
relocated | It replayed, but an element was found somewhere other than recorded. |
handed-off | The replay stopped matching, so the model took the step over. |
missed | There was no recording, so the model ran. |
stale | A recording of this step exists under another key, so it wasn't used. |
diverged | Under strict replay, the recording no longer replays. |
off | The 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| Note | What it means |
|---|---|
recorded | The new recording was kept. The next run replays it. |
not kept: the test failed | A step failed before any check confirmed this one. |
not kept: no check after it passed | Nothing 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-5To 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 || Reason | What changed |
|---|---|
… is no longer on the page | The element was renamed or removed. |
… matches 2 elements | The description now fits more than one element. |
effect: expected …, saw none | Every action ran, but the change it made last time didn't happen. |
"…" was not shown within … ms | The 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 staleRerun 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:
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 clearls lists the recordings, stats counts them by outcome, and clear deletes them so every step
records afresh.