DEV Community

Jack M
Jack M

Posted on

AI Agent Trace Replay: Turn Production Failures Into Safe Regression Tests

Your agent failed once in production. The tempting fix is to patch the prompt, glance at the next successful run, and move on. That is how the same failure returns after a model, tool, or routing change.

The more useful response is to turn the failed run into a replayable regression test. Not a full rerun against live customer data. Not a screen recording. A small, scrubbed fixture that preserves the decisions and tool boundary that mattered, then proves the fix without sending another email, creating another ticket, or charging another card.

This guide shows how to build that pipeline. It is aimed at engineers operating tool-using agents in production. The result is a durable feedback loop: incidents become tests, and tests make the next release safer.

What agent trace replay is — and is not

An agent trace is an ordered record of a run: user intent, policy version, prompt revision, model calls, tool requests, tool responses, retry decisions, and final outcome. Trace replay uses a captured run as controlled input for a later test.

That definition matters because replay can mean different things:

Replay mode What stays fixed What you learn
Deterministic fixture replay Tool outputs and selected model output Whether orchestration, parsing, policies, and recovery still work
Model-in-the-loop replay Tool outputs are fixed; model runs again Whether a prompt/model change preserves the expected trajectory
Load replay Request timing and token shapes Whether serving latency, cache behavior, and capacity changed
Live rerun Almost nothing Usually too risky for debugging a customer incident

For a regression suite, start with the first mode. It makes a production bug repeatable without repeating the side effect. Add model-in-the-loop replay only after you can score it reliably.

This is different from a generic evaluation harness. An evaluation suite begins with representative tasks. Trace replay begins with a real failure, preserves the evidence that explains it, and turns a specific incident into a release gate. It is also different from event sourcing: the goal here is not to reconstruct every business object, but to create a minimal, privacy-safe test case for the agent behavior.

The failure pattern that replay catches

Imagine an operations agent that receives a request to pause a subscription. A billing lookup times out, the agent retries with a malformed account identifier, then tells the customer the subscription was paused. The retry did not change the account, and the customer is still billed.

An answer-quality check may pass because the message is polite. A unit test for the billing tool may pass because the tool validates normal inputs. The failure lives in the trajectory: timeout, incorrect fallback identifier, write attempt, unverified success claim.

A replay fixture lets you test that exact sequence:

  1. Return the recorded timeout from the first lookup.
  2. Provide the recorded account data from the fallback lookup.
  3. Require an operation receipt before a success response is allowed.
  4. Assert that the agent either verifies the pause or reports an unresolved state.

The key idea is simple: assert what the agent did and what it was allowed to claim, not only its final prose.

Capture the smallest useful trace

Do not dump every prompt and response into a test repository. A useful capture is intentional and bounded. Store only what you need to reproduce the decision boundary.

type TraceEvent = {
  runId: string;
  sequence: number;
  kind: "input" | "model" | "tool_call" | "tool_result" | "policy" | "outcome";
  at: string;
  version: { prompt: string; model: string; tools: string; policy: string };
  payloadRef?: string;       // encrypted or redacted fixture reference
  tool?: { name: string; operationId?: string; idempotencyKey?: string };
  digest: string;            // tamper-evident hash of canonical event data
};
Enter fullscreen mode Exit fullscreen mode

Use a separate protected store for raw payloads if you genuinely need them. The trace used for test selection can usually carry redacted values, stable identifiers, schemas, response classes, and hashes. Never record chain-of-thought. You do not need it to verify tool order, policy decisions, structured arguments, or an authoritative receipt.

Capture these fields at minimum:

  • A correlation ID distinct from the customer and conversation IDs.
  • Versions for the prompt, model, tool schema, retrieval corpus, and runtime policy.
  • Tool arguments after validation, with secrets and personal data replaced by typed placeholders.
  • Tool result class: success, retryable failure, permanent failure, unknown outcome, or rejected.
  • External operation ID and authoritative verification result for every write.
  • The final customer-visible claim and the rule that allowed it.

The recording should be append-only. If a trace is edited for a fixture, retain its source run ID and record each redaction. Otherwise a convenient test fixture can quietly stop representing the incident that justified it.

Build a side-effect firewall before replaying anything

The dangerous mistake is to point a replay at production tools and assume an idempotency key makes it safe. Idempotency reduces duplicate writes; it does not make an old action appropriate today.

Use a replay adapter that has an explicit mode and a deny-by-default tool policy.

const SAFE_REPLAY_TOOLS = new Set(["customer.lookup", "billing.status"]);

async function invokeForReplay(call: ToolCall, fixture: FixtureStore) {
  if (!SAFE_REPLAY_TOOLS.has(call.name)) {
    return {
      status: "blocked",
      reason: `Replay may not execute ${call.name}`,
      required: "recorded_fixture_or_verified_sandbox"
    };
  }

  const recorded = await fixture.match(call.name, call.normalizedArgs);
  if (!recorded) return { status: "missing_fixture" };
  return recorded.result;
}
Enter fullscreen mode Exit fullscreen mode

Treat writes, browser actions, emails, payment actions, deployments, and permission changes as blocked by default. For a write you need to model, return a recorded receipt or run against a dedicated sandbox with synthetic identities. A good replay failure is missing_fixture; it tells you the test is no longer a faithful simulation. A bad replay silently calls the real service.

Turn an incident into a compact fixture

Raw production runs are noisy. Reduce them in a reviewable sequence.

1. Name the observable failure

Avoid labels such as “agent confused.” Write the event that should never recur: “The agent claimed a subscription was paused without a successful pause receipt.” This becomes the primary assertion.

2. Keep the causal path, not every span

Retain the user request, relevant retrieval result, tool calls, tool responses, policy decisions, and final output. Remove unrelated telemetry and tool calls that cannot affect the outcome. If deletion changes the failure, put it back.

3. Replace sensitive values consistently

Map alex@example.com to user_01@example.invalid everywhere. Preserve the shape, locale, formats, and relationships that affected parsing. Do not replace a malformed identifier with a clean one when malformed input triggered the bug.

4. Freeze external responses

Represent each tool result as a fixture, including error bodies and headers the agent reads. A generic 500 does not reproduce a timeout branch that contained a retry hint.

5. Version the expected contract

Keep the allowed trajectory with the fixture. The contract can specify required calls, forbidden calls, maximum retries, expected state, and required evidence for a success claim.

case: pause-with-unknown-outcome
must_call:
  - customer.lookup
  - billing.pause
  - billing.get_operation
forbid_call:
  - customer.notify_success
max_calls:
  billing.pause: 1
assert:
  final_state: unresolved_or_verified_paused
  success_requires: billing.get_operation.status == completed
Enter fullscreen mode Exit fullscreen mode

This format is intentionally boring. It lets a reviewer reason about behavior without reverse-engineering a prompt transcript.

Use layered assertions, not one brittle snapshot

An exact full transcript comparison makes every harmless wording change look like a regression. A single LLM judge makes every serious policy violation subjective. Combine layers instead.

  1. Structural assertions: valid tool names, JSON schema, required fields, and no secret-like values in output.
  2. Trajectory assertions: tool order, retry ceiling, no forbidden tool, and approval step before a sensitive action.
  3. State assertions: an independent fixture or sandbox read-back confirms the intended business state.
  4. Outcome assertions: the response does not promise more than evidence supports.
  5. Semantic assertions: use a calibrated judge only for meaning that cannot be expressed deterministically, such as whether an explanation is understandable.

Here is a deliberately small trajectory test:

expect(events.toolCalls.map(x => x.name)).toEqual([
  "customer.lookup", "billing.pause", "billing.get_operation"
]);
expect(events.toolCalls.filter(x => x.name === "billing.pause")).toHaveLength(1);
expect(events.final.claim).not.toMatch(/paused successfully/i);
expect(events.final.claim).toMatch(/still verifying|could not confirm/i);
Enter fullscreen mode Exit fullscreen mode

Prefer stable properties over wording. If your product truly needs a specific compliance phrase, assert that phrase separately and explain why.

Make model-in-the-loop replay measurable

Eventually you will want to replay the same fixture after changing a model or prompt. That run is non-deterministic, so design for variance instead of pretending it does not exist.

Run the case several times at the production-like temperature. Require every run to satisfy safety invariants, then set a threshold for softer outcomes. For example, all five runs must avoid an unverified success claim, four of five must choose the verified recovery path, and a human reviews any new tool sequence before it becomes accepted behavior.

Record the model and decoding settings with the result. A changed temperature, tool description, or retrieval revision can cause divergence even when the prompt file is untouched. If a replay diverges, classify it: acceptable alternative, new regression, fixture mismatch, or insufficient specification. Do not hide divergence by updating the snapshot automatically.

Put replay in the release path

Not every captured trace belongs in pull-request CI. Start with a small “incident corpus”: the highest-impact, recent, and recurring failures. Tag each case by workflow, risk, tenant shape, and the dependency it guards.

Run the cheap deterministic fixture suite on every change that touches prompts, policies, tool schemas, orchestration, or response rendering. Run model-in-the-loop cases on a scheduled or pre-release job where repeated samples are affordable. Run load replay separately; it answers latency and capacity questions, not behavioral correctness.

When a replay fails, show a compact diff:

case: pause-with-unknown-outcome
expected: verify operation before customer success claim
actual:   billing.pause -> customer.notify_success
first divergence: sequence 7, policy "receipt_required" skipped
changed inputs: prompt v42 -> v43; tool schema unchanged
Enter fullscreen mode Exit fullscreen mode

That output is useful to the engineer on call. A wall of token traces is not.

Common replay traps

Treating replay as proof of live behavior. A fixture proves behavior against its captured conditions. It does not prove correctness for new customer data, a changed upstream API, or a different model response. Keep ordinary integration tests and sampled production monitoring.

Saving raw customer data forever. Expiring raw captures and retaining redacted fixtures reduces risk. Define who can access source traces and how a fixture is deleted when its underlying data must be removed.

Mocking away the failure. If the incident involved a malformed response or a missing field, preserve it. A happy-path mock creates false confidence.

Testing only the final answer. Agents can say the right thing after taking the wrong action. Test the trajectory and authoritative state.

Letting fixtures drift. Give every fixture an owner, source trace, schema version, and review date. When a tool contract changes, decide whether the old incident remains a valid regression or should be migrated with an explicit explanation.

A practical first week

Pick one incident where the agent caused an incorrect claim, a repeated action, an unsafe call, or a costly loop. Capture the trace, redact it, block all side effects, and write three deterministic assertions: one forbidden action, one required evidence check, and one final-state rule. Put that one case in CI.

Then repeat for the next real incident. In a few weeks, you will have a test corpus that reflects the failures your system actually has—not a generic benchmark someone else designed. That is the durable value of replay: each incident makes the next release a little harder to break in the same way.

FAQ

What is AI agent trace replay?

AI agent trace replay reruns a captured agent scenario using recorded or simulated inputs. It is used to reproduce a failure, compare behavior after a change, and make an incident into a regression test without invoking real side effects.

Should an agent replay call real tools?

Usually no. Default to recorded fixtures and deny real writes, emails, payments, deployments, and permission changes. Use an isolated sandbox with synthetic data only when the behavior cannot be tested from a recorded response.

What should an agent trace contain?

Keep ordered events, component versions, validated tool arguments, result classes, policy decisions, external operation receipts, and the final claim. Redact sensitive values and avoid storing private reasoning or unnecessary prompt content.

How do you test a non-deterministic agent with replay?

Test deterministic safety and trajectory rules on every run, then sample multiple model-in-the-loop runs for softer quality signals. Treat a new tool path as a review event rather than silently accepting a new snapshot.

Is trace replay the same as agent observability?

No. Observability helps you inspect what happened. Trace replay turns selected evidence from what happened into a controlled test you can run before the next release. Strong systems use both.

How many replay fixtures should a team start with?

Start with one high-impact incident and add cases after real failures. A small, maintained incident corpus is more valuable than a large collection of stale traces no one can explain.

Top comments (1)

Collapse
 
galagent profile image
Qingfeng •

Strong piece. The part I would push on: pause-with-unknown-outcome is testable at all only because the write carried an operation ID you could query afterwards. Teams that record traces but never design writes for unknown-outcome recovery end up with a fixture that faithfully proves the retry double-charged. Also, SAFE_REPLAY_TOOLS with deny-by-default and missing_fixture as a legitimate failure is exactly the policy the production tool boundary should have had in the first place, not just the replay adapter. A test that cannot call billing.pause is a great test; a live agent that could call it with no approval step was the actual bug. Replay catches the failure you already saw, the runtime guardrail has to catch the first occurrence.