AI agents · Deterministic replay · Agent debugging

Deterministic Replay for Nondeterministic AI Agents

Record model, tool, time, randomness, configuration, and state dependencies so agent failures can be replayed without repeating real-world effects.

Published
Updated
Code examples
Illustrative
Reading time
5 minutes

Deterministic agent replay does not mean forcing a hosted model to generate the same tokens again. It means re-executing orchestration against recorded model responses, tool results, time, randomness, approvals, and initial state so the same observable decisions can be inspected without repeating external effects.

Use replay to debug and regression-test a captured run. Use fresh repeated trials to estimate current model behavior; replay cannot measure a distribution it has frozen.

Define three different replay modes

Teams often call all of these “replay,” but they answer different questions:

ModeExternal dependenciesBest question
Trace inspectionnone; render recorded eventsWhat happened?
Offline deterministic replayreturn recorded responsesDoes orchestration reproduce the same decisions?
Live re-executioncall current models/tools in a sandboxDoes the current system still solve the case?

Only the middle mode should promise deterministic behavior, and only within a specified event contract. Live re-execution is intentionally nondeterministic and may see changed data.

Temporal’s workflow model demonstrates the core pattern: deterministic workflow code is replayed against an event history, while nondeterministic external operations are placed in activities whose recorded results are delivered during replay instead of being re-executed. Temporal workflow determinism documentation

Capture every source of observable nondeterminism

A replay bundle needs more than the prompt:

interface ReplayBundle {
  readonly formatVersion: string;
  readonly runId: string;
  readonly startedAt: string;
  readonly config: {
    readonly codeRevision: string;
    readonly model: string;
    readonly promptDigest: string;
    readonly toolSchemaDigest: string;
    readonly policyDigest: string;
    readonly decoding: Readonly<Record<string, unknown>>;
  };
  readonly initialStateDigest: string;
  readonly events: readonly ReplayEvent[];
}

type ReplayEvent =
  | { readonly seq: number; readonly type: "clock"; readonly value: string }
  | { readonly seq: number; readonly type: "random"; readonly value: number }
  | { readonly seq: number; readonly type: "model"; readonly requestDigest: string; readonly response: unknown }
  | { readonly seq: number; readonly type: "tool"; readonly requestDigest: string; readonly response: unknown }
  | { readonly seq: number; readonly type: "approval"; readonly value: unknown }
  | { readonly seq: number; readonly type: "effect"; readonly idempotencyKey: string; readonly outcome: unknown };

This is illustrative. A production format also needs content types, error variants, cancellation, streaming boundaries where behavior depends on them, redaction metadata, integrity digests, and references to encrypted large payloads.

Capture:

  • complete model request after templating, or a reconstructable encrypted reference;
  • provider-returned model identifier and usage metadata;
  • every tool request, result, error, and timeout decision;
  • initial database, retrieval, memory, and feature-flag snapshots or fixture digests;
  • clock reads, generated IDs, random draws, scheduling decisions, and deadlines;
  • user and human approval events; and
  • code, dependency, prompt, schema, grader, and policy versions.

A temperature of zero is not a replay mechanism. Provider infrastructure, model aliases, safety layers, and request handling can change; recording the response is the reliable boundary.

Make effects impossible during offline replay

The replay runner must use an entirely separate capability set:

interface ToolPort {
  call(name: string, args: unknown): Promise<unknown>;
}

class RecordedToolPort implements ToolPort {
  constructor(private readonly events: readonly ReplayEvent[]) {}

  async call(name: string, args: unknown): Promise<unknown> {
    const digest = digestRequest(name, args);
    const match = this.events.find(
      (event) => event.type === "tool" && event.requestDigest === digest,
    );
    if (!match || match.type !== "tool") {
      throw new Error(`Unrecorded tool request: ${name}`);
    }
    return match.response;
  }
}

digestRequest is deliberately omitted: use a documented canonical serialization, because ordinary object stringification can vary with property order and types.

Do not merely set DRY_RUN=true inside live tool code. Construct the offline runner without network credentials, production DNS access, writable mounts, or real message queues. If orchestration requests an unrecorded effect, stop with a divergence rather than improvising.

Detect divergence at the first boundary

At each model or tool request, compare the current normalized request to the recorded request. Report:

first divergent event sequence
expected and actual request digests
structured diff after secret redaction
current and recorded code/config fingerprints
preceding event IDs

Continue-after-divergence can help exploration, but label the resulting run non-canonical: once the path changes, later recorded responses may no longer be valid for the new requests.

Decide what normalization may ignore. Trace IDs and timestamps may be incidental; tenant, authorization, source provenance, tool arguments, and idempotency keys are usually semantic. Version the normalization rules.

Handle streams and concurrency explicitly

If the orchestrator acts only after a complete model response, storing the final structured response may suffice. If it displays or acts on partial events, record boundaries, order, cancellation, and errors.

Concurrent branches make global ordering unstable. Assign each event a branch identifier and causal parents. Replay dependency order, not accidental wall-clock interleaving, unless the race itself affects behavior. Record tie-breaking decisions made by “first result wins” logic.

For long-running durable agents, event histories can grow. Use immutable segmented logs and snapshots, but retain enough history to verify snapshot provenance. The durable agents guide explains idempotency and resumable effects.

Turn incidents into portable regression cases

After reproducing a failure:

  1. identify the minimal causal slice;
  2. replace personal and proprietary content while preserving behavior;
  3. create an isolated initial fixture;
  4. encode the expected outcome and forbidden effects;
  5. run the recorded path to verify the harness; and
  6. run fresh candidate trials to test the fix.

Recorded responses are useful for an orchestration unit test. They are not proof that a changed model will respond similarly. Keep a separate live or sandboxed evaluation for that claim.

Secure the replay system

Replay bundles concentrate prompts, retrieved documents, tool results, approvals, and potentially secrets. Treat them as sensitive production data:

  • collect the minimum content required;
  • tokenize or redact secrets before persistence;
  • encrypt payloads and restrict decryption by role;
  • isolate tenants and environments;
  • sign or hash event segments to detect tampering;
  • set retention and deletion procedures; and
  • prevent untrusted recorded content from becoming executable instructions in analysis tools.

OpenTelemetry treats prompt, completion, tool-call, and tool-result content as opt-in because it may be sensitive or expensive. Apply the same caution to replay capture. OpenTelemetry GenAI observability guidance

Replay checklist

  • Name whether you mean inspection, offline replay, or live re-execution.
  • Record external results instead of calling them during offline replay.
  • Fingerprint code, prompt, model, schemas, policy, fixtures, and decoding.
  • Virtualize time, randomness, IDs, scheduling, and feature flags.
  • Construct replay without production credentials or writable integrations.
  • Stop on unrecorded requests and report the first divergence.
  • Model causal order for concurrent branches.
  • Separate deterministic orchestration tests from fresh stochastic evals.
  • Minimize, encrypt, isolate, audit, and expire replay content.