DEV Community

Tisha
Tisha

Posted on

You Recorded the Incident. Now Prove Your Fix Actually Works.

Part 2 of Your Agent Failed in Prod. Good Luck Reproducing It.

This work was presented at the AI Engineer World's Fair 2026 by Susheem Koul and Tisha Chawla.


In Part 1 the agent deleted the wrong record at 9:04, and you could not make it happen again. The fix was not to force the model to be deterministic. It was to record the run: the exact prompt, the sampled completion, the tool calls, the retrieved chunks, the pinned model version. Freeze the run, not the model.

So you did that. The incident is now a file on disk. You can open it and see, precisely, what the agent decided that night.

Now comes the part nobody talks about. You still have to fix the bug. And you have to prove the fix works. This is where most teams quietly fall back into the swamp they just climbed out of.

The trap, one more time

You change the code. Now you want to confirm it is fixed. So you run the agent again and watch.

Stop. You just walked back into the trap.

The moment you re-run to check your fix, you are regenerating. The model samples a fresh path. The retrieval returns slightly different chunks. The batch shape on the endpoint is different from the batch shape at 9:04. Your "it works now" is one draw from a distribution, and so was the failure. You have proven nothing, and you know it, because the failure never reproduced on demand in the first place.

To verify a fix you need the exact opposite of a re-run. You need everything about that incident to stay frozen, except the one thing you changed.

A scalpel, not a re-run

That is the whole idea, and it has a name: cut-point replay.

Take the recorded incident. Replay it. But mark one boundary as the cut-point. Everything upstream of it is served from the recording, byte for byte the same inputs the agent saw that night. The boundary you changed runs your new code, live. Everything downstream runs live too, so you can watch what your fix does to the rest of the run.

You are not re-running the agent. You are dropping your new code into the middle of a frozen incident and asking one question: given exactly what happened up to this point, does my change do the right thing now?

No model call. No API cost. No flakiness. The same incident, every time, forever.

Walking through it

Here is the deletion agent from Part 1. Two boundaries: the model decides, the tool acts.

from chronicle import boundary, reset_session, ReplayPlan
from chronicle.envelope.store import EnvelopeStore

@boundary("agent", kind="llm")
def agent(state: dict) -> dict:
    ...  # calls the model, returns a decision and tool calls

@boundary("delete_file", kind="tool")
def delete_file(path: str, environment: str) -> dict:
    ...  # the ungated tool that wiped prod at 9:04
Enter fullscreen mode Exit fullscreen mode

You already recorded the incident and froze it as a fixture:

session = reset_session()
session.store = EnvelopeStore(".chronicle/runs/incident.jsonl")
session.begin_trace("deletion-incident-001")
run_agent(...)                                    # the bad run, captured
session.export_trace("fixtures/traces/deletion-incident-001/")
Enter fullscreen mode Exit fullscreen mode

The recorded graph is exactly what you would expect:

agent@1  ->  delete_file@1 (deleted prod)  ->  agent@2
Enter fullscreen mode Exit fullscreen mode

Now the fix. One guard, in the tool:

@boundary("delete_file", kind="tool")
def delete_file(path: str, environment: str) -> dict:
    if environment == "production":
        return {"blocked": True, "reason": "guard: destructive op refused in prod"}
    ...
Enter fullscreen mode Exit fullscreen mode

And here is the test that proves it, against the real incident:

session = reset_session()
session.load_trace("fixtures/traces/deletion-incident-001/")
session.enable_replay(
    ReplayPlan()
    .stub("agent", 1)         # upstream: the exact decision the model made at 9:04
    .live("delete_file", 1)   # cut-point: your new gated code runs for real
    .live("agent", 2)         # downstream: watch what the agent does after the block
)
run_agent(...)

assert session.captured_result("delete_file", 1)["blocked"] is True
Enter fullscreen mode Exit fullscreen mode

Read the plan again, because it is the entire point. agent@1 is stubbed: the model does not run, you replay the decision it actually made that night. delete_file@1 is live: your new guard executes against those exact arguments. agent@2 is live: you get to see how the agent reacts to a refused deletion instead of a successful one.

You changed one boundary and held the rest of history still. If the guard blocks the delete, the fix works. Not "worked once." Works, deterministically, on the recorded incident, in CI, with no API key.

The two layers, put to work

Part 1 argued for testing in two layers. Cut-point replay is Layer 1 in action: structural, deterministic, about control flow and tool safety. Did the right tool get called? Were the arguments shaped correctly? Was the destructive action refused? None of that needs the model, so none of it flakes.

Layer 2 is for the questions structure cannot answer. If your fix was a prompt rewrite or a model bump, "is the output still correct" is a judgment, not an equality check. That is where an LLM-as-judge scores the new completion against the recorded gold one for meaning, not for bytes. Use Layer 1 to prove the machinery is right. Use Layer 2 to prove the words are still good.

The incident becomes a regression test

Here is the quiet payoff. That fixture under fixtures/traces/ is committed to git. It is now a permanent test. Six months from now, when someone refactors the tool router and the guard silently stops firing, this test goes red on their pull request, not on a customer's production database.

The failure that was unreproducible at 9:04 becomes a green check that runs on every commit. That is the difference between an incident and a regression test: one is a story you tell, the other is a thing your CI enforces.

Before you commit anything: redact

A recorded run is a faithful copy of production. It contains the assembled prompt, the retrieved chunks, the tool arguments. That means it can contain customer names, emails, API keys, internal URLs, whatever your agent touched.

You cannot commit that to git raw. Security and legal are right to block it, and a leaked secret in a fixture is a real incident of its own.

So redaction is not a nice-to-have on the recording path, it is a gate. Scrub secrets and PII out of the envelope before it is written, keep the shape and the structure that your tests assert on, and drop the sensitive values. A recording you cannot safely commit is a recording you will not use.

What this does not fix

Be honest with yourself about the boundary of the technique.

Cut-point replay fixes bugs in your code: routing, guards, argument assembly, tool safety, orchestration. It reproduces those perfectly and lets you verify a fix cheaply.

It does not fix a bad generation. If the model hallucinated a refund amount, replay will faithfully serve that hallucination back to you. Fixing that lives in Layer 2 and in prompt and model work, not in deterministic replay.

Fixtures also drift. Treat them like snapshot tests: when the prompt or the schema changes on purpose, the fixture has to be re-recorded on purpose. And hosted model drift is still outside your control, which is exactly why you pin the version in the envelope so you at least know when it moved.

TLDR;

  1. Record at the boundary, the full run, not just the prompt. (Part 1.)
  2. Reproduce the incident by replaying the recording, no model call.
  3. Pick the boundary you changed. That is your cut-point.
  4. Stub everything upstream from the recording. Run the cut-point live. Watch downstream.
  5. Assert on the cut-point result, the blocked flag, the tool call, the argument, not on the final prose.
  6. Commit the trace as a regression test so the incident can never quietly return.
  7. Redact before you commit. Always.

If you want the tooling instead of building it yourself, our team has put the record, cut-point replay, and two-layer verification into an open-source library called Chronicle: github.com/theagentplane/chronicle. It is early and honest about its limits. Issues and war stories welcome.

Official PyPi Release Here

Install to record and replay your agentic workflow

pip install agent-chronicle
Enter fullscreen mode Exit fullscreen mode

Top comments (18)

Collapse
 
innovationsiyu profile image
Siyu

The principle of freezing the run rather than the model is something I think about a lot. In Opportunity Skill, impressions work on a similar logic. You capture what actually happened in a collaboration, the exact preferences expressed, the precise boundaries stated, and commit it as an immutable semantic unit. No editing in place, only create and prune. That recorded signal stays accurate over time, exactly like your incident fixture staying green in CI. The parallel between deterministic replay and a trustworthy professional record is stronger than most people realize.

Collapse
 
tisha profile image
Tisha

yes its an effort to make our MAS workflows more deterministic for better reliability.

Collapse
 
raju_dandigam profile image
Raju Dandigam

"Freeze the run, not the model" is the sentence people should steal from this. Replaying the exact prompt, sampled completion, retrieved chunks, and tool outputs turns an anecdote into a regression artifact, which is the only way fixes stay honest once the model backend moves under you. The other production lesson is to make redaction part of the capture path, not a cleanup step later. Otherwise the runs that are most valuable for debugging become the ones nobody can safely share.

Collapse
 
tisha profile image
Tisha

Agreed!

Collapse
 
teoh_cheeho_2ff3048fc1af profile image
Teoh Chee Ho

Hi Tisha,
Can we contact for each other?

Thread Thread
 
tisha profile image
Tisha

sure!

Thread Thread
 
teoh_cheeho_2ff3048fc1af profile image
Teoh Chee Ho

I just emailed to you. Please kindly check

Thread Thread
 
tisha profile image
Tisha • Edited

Hey! What mail did you use?

Collapse
 
hannune profile image
Tae Kim

The cut-point replay pattern solves the exact problem I kept running into when testing LangGraph-based agents: a fix that looked correct in a fresh re-run would fail again in production because the re-run sampled a different decision path than the original incident. Freezing everything upstream of the changed boundary and running only the modified node live against the recorded inputs is structurally the same as what LangGraph's checkpoint replay gives you when you pin a trace and re-enter at a specific node, though it requires you to discipline the fixture pipeline the way you describe. The redaction gate before committing fixtures is the piece most teams skip and then discover when a PII incident shows up in their git history.

Collapse
 
tisha profile image
Tisha

Glad this helps!

Collapse
 
nazar-boyko profile image
Nazar Boyko

The replay plan keys off call order (agent@1, delete_file@1), so I'm curious what happens when the fix changes how many times a boundary runs. A guard that makes the agent retry once would shift every index after it, and I can't tell from the example whether that fails loudly or just quietly stubs the wrong call.

Collapse
 
tisha profile image
Tisha • Edited

Good question.

The plan keys each call by the boundary name plus a per name counter, not by flat position. In replay the wrapper keeps one counter for each boundary name and looks up the fixture by name and index. So when your fix adds a retry, only that boundary's counter moves. agent@1 and agent@2 keep their identity, and the retry just appends delete_file@2 instead of shifting everything after it.

When a call has no stub entry in the plan, the wrapper runs it live. It does not raise, and it does not reuse another recorded output. So your appended delete_file@2 runs the gate for real instead of getting @1's envelope. That specific case is safe.

The one real gap is that nothing compares the recorded call count to the live count. So a wrong stub is only possible if a fix inserts an extra call of a boundary you already stubbed, and inserts it before a later recorded call of that same name. Then the indexes shift and the earlier call quietly gets its neighbor's envelope. The cut point demos avoid this by stubbing only the upstream call and running everything downstream live.

So to answer directly: it does not fail loudly, but it also does not silently stub the wrong call in the retry example you gave. The only silent mismatch is the stub before a later same name call case above, and adding a check that recorded and live counts match per boundary would close it.

Source code : github.com/theagentplane/chronicle

Collapse
 
eduzsh profile image
Edu Peralta

Cut point replay is the right instinct. The failure mode I keep hitting is different though. It is not the retest that is broken, it is that most teams never actually define the cut point in the first place. When an agent misbehaves, the fix usually lands as a system prompt tweak or a retry wrapper, and nobody isolates which function actually changed. I have started treating any agent fix the way I treat a diff review, asking what specific code path moved and what stayed frozen before I even look at whether the new output looks better. Freezing the recording and replaying only the changed function is the only way I have found to tell a real fix from a lucky sample.

Collapse
 
tisha profile image
Tisha

This matches what I see, and I think there is a structural reason the cut point never gets defined. The two fixes you named are exactly the ones that do not map to a single function. A prompt tweak changes what you feed the boundary, so it lives upstream in the input assembly. A retry wrapper changes how often boundaries fire, so it lives in the orchestration between them. Neither is "change this function and freeze the rest," so the isolation step gets skipped. If you cannot name what stays frozen, you do not have a fix yet. You have a new distribution and one sample from it, and naming the frozen set is the part people skip because it is harder than editing the prompt.

The ordering is the part I would underline. Judging whether the output got better first is the lucky sample trap, because a live rerun lets the model reroll everything and you cannot separate your diff from the noise. Freezing the recording and replaying only the changed path holds the model constant, so any difference is attributable to your code and not a good roll.

Collapse
 
jam-techcirkle profile image
James Sanderson

"Freeze the run, not the model" is the cleanest statement of this I've read — and the trap you name, that re-running to verify a fix is just drawing one fresh sample from the same distribution, is exactly where I've watched teams fool themselves. The hard part in practice is the boundary of what you replay: the prompt and sampled completion are easy to pin, but tool calls that hit live state (a DB that's moved on since 9:04) are where the frozen replay starts to leak. Do you stub those tool responses from the recording too, or draw the line at the model boundary and let the tools re-execute? That choice seems to decide whether "everything frozen except the one thing you changed" actually holds.

Collapse
 
tisha profile image
Tisha

Think of it as changed versus unchanged, not model versus tools. The one thing you changed runs live. Everything else plays back its recorded output, tools included. So that DB tool gets stubbed from the recording, because if you let it run live it reads today's data, and now the agent changes for two reasons at once, your fix and the moved-on world. You cannot tell which caused it, which is the exact thing you were trying to avoid. A test of a past incident should use the world as it was at 9:04, not now.

The one exception is when the tool itself is what you changed. Then you run its new code, but still on the recorded inputs, and check what it decided rather than letting it touch anything real.

Collapse
 
kartik-nvjk profile image
Kartik N V J K

Freezing everything upstream of the fix boundary and only running the changed code live is what makes this actually provable, because re-running the whole agent just resamples and you can pass without having touched the original failure. The cut-point also turns into a natural place to inject counterfactuals, so once the fixture captures the exact prior context you can ask whether the guard still holds under a slightly reworded input. Are you storing the full trace capture, or just the boundary inputs?

Collapse
 
tisha profile image
Tisha • Edited

Both, and I think they are the same thing at different zoom levels. The unit we store is the boundary, and for each one we capture its inputs and its output, not just the inputs. The full trace is just those per boundary envelopes linked by parent and order.

You need both sides for this to work. The recorded outputs are what let you freeze everything upstream, and the inputs are what let you run the changed boundary live and reword them for counterfactuals. Each envelope holds the assembled prompt, graph state, and retrieved chunks going in, and the completion or tool result coming out, plus the model version and sampling params so a stub returns exactly what was seen.

That is also why the cut point doubles as a counterfactual spot. The exact prior context is already pinned in the envelope, so you can reword one input, keep every other boundary frozen, and see whether the guard still holds.

Repo - github.com/theagentplane/chronicle