Replay an agent run

Goal. Answer “what did it know when it did that?” for any step of a past run. About five minutes.

You need. A run that recorded its tool calls as events and its state as data. Record tool activity sets that up.

The join

Every event carries the commit version it was appended at, and every read takes --as-of. Those two facts together are the whole technique: the log tells you when, and --as-of tells you what.

strata ./agent --json event list
seq 0  version 3  run.start  {"goal": "fix the bug"}
seq 1  version 5  tool.call  {"path": "app.py", "tool": "read"}
seq 2  version 7  tool.call  {"path": "app.py", "tool": "edit"}
seq 3  version 9  run.end    {"status": "ok"}

Step through it

Read the state as of each event’s version:

strata ./agent kv get state.step --as-of 5
strata ./agent kv get state.step --as-of 7
strata ./agent kv get state.step --as-of 9
1
2
3

That is the run reconstructed. At the moment it called read, the state was 1. By the time it called edit, 2. Nothing was restored and nothing moved: these are reads.

Before the first write, a key simply does not exist yet, which is why --as-of 3 returns nothing rather than an error.

The log and the state must be on the same branch

The join works because the events and the reads above are on one branch. Versions are per branch once branches diverge, so an event appended to default and a read taken on a run branch are counting different clocks, and asking for one against the other raises HistoryUnavailableError:

strata ./agent --branch run-2 kv get state.step --as-of 12
history_unavailable.engine.persistence_history: requested persistence history is
unavailable

So if runs happen on branches, keep each run’s log on its own branch, where its state is. Build an agent with rewindable state works through that shape.

That sits in tension with writing the log to default so a merge cannot eat it, and the resolution is to pick per purpose: a log you will replay against state goes on the branch, and a cross-run index goes on default. Neither survives a merge, so the branch is what you keep.

Why this is not just logging

A log tells you what the agent did. This tells you what it was looking at when it decided to, which is the question you actually have when a run goes wrong. Reproducing that from a log alone means replaying every write in order and hoping nothing was missed.

It also holds for every model at once, because the commit timeline is shared:

strata ./agent json get plan '$' --as-of 7
strata ./agent graph bfs deps app.py --as-of 7

Same instant, three different shapes of data.

Fork the moment to work in it

Reading the past does not let you try a different continuation. For that, branch from it:

strata ./agent branch fork default what-if --version 7

Now what-if starts exactly where the agent was before its second tool call, and you can run a different attempt from there. Fork from historical state covers the details.

What just happened

Nothing was written. Time travel is a read, so replaying a run has no effect on the run, the database, or anything reading it concurrently.

Failure modes

history_unavailable.engine.persistence_history. The version is outside what the database retains, or a wall-clock value was passed to --as-of, which wants a commit position. Take the number from the event’s version field, not its timestamp, which is the occurrence clock.

The events are gone. They were on a run branch that got merged. See Record tool activity.

State reads return nothing at every version. Check the key existed on the branch you are reading. kv history on that branch is the fastest check.

Next

Time travel for the two clocks, and Events for what the log guarantees.

Verified against strata-core@v1.2.7 · Edit this page · Report an issue

agents: this page as markdown → /docs/guides/agents/replay-an-agent-run.md