# Determinism (/docs/determinism)



Determinism is one concept in the **safety & determinism**
[thematic island](/docs/concept-clusters#what-lives-outside-the-cluster-pattern) —
the others are [safety](/docs/safety), [scrubbers](/docs/scrubbers),
[fabrication detection](/docs/fabrication-detection), and
[fingerprint](/docs/fingerprint). Each carries its own mechanism;
no natural cluster triplet.

A recorded turn replays byte-identical against the same package
version + the same input. That's the property
`@pleach/eval@0.1.0` and `@pleach/replay@0.1.0` are built
around — both ship real bodies for the entry-point surfaces
(`replayTurn`, `verifyChainIntegrity`, `compareScored`,
`diffReports`); remaining slices throw typed `NotImplemented`
sentinels per [Packages](/docs/packages). It's the reason for
several otherwise-confusing decisions across the substrate —
sync-only stream observers, quantized temperature buckets,
canonicalized JSON, deterministic reducers, ULID record ids.

This page tells the story end-to-end. Each primitive's
contribution to the chain shows up in its own reference page;
here they're connected.

<SourceMeta
  source="[
  { label: &#x22;src/fingerprint/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/fingerprint&#x22; },
  { label: &#x22;src/replay/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/replay&#x22; },
  { label: &#x22;src/streaming/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/streaming&#x22; },
]"
/>

## The chain [#the-chain]

<Mermaid
  chart="flowchart LR
    Input([turn input]) --> Compose[composeBudgetedPrompt]
    Compose -->|deterministic bytes| FP[computeFingerprint]
    FP -->|hash| Cache{cache hit?}
    Cache -->|yes| Recorded[recorded result]
    Cache -->|no| Provider[provider invoke]
    Provider -->|stream chunks| Observers[sync observers]
    Observers -->|verdicts| Channels[channel updates]
    Channels -->|deterministic reducers| State[session state]
    State --> Ledger[(AuditableCall row)]"
/>

Same input → same composed prompt → same fingerprint → same
cached or recorded result → same observer verdicts → same channel
state → same audit row.

Each arrow in the chain is a contract. Break any one and replay
determinism is gone for the whole turn.

## The five contracts [#the-five-contracts]

### 1. Composed prompts are deterministic [#1-composed-prompts-are-deterministic]

`composeBudgetedPrompt` quantizes the active budget, sorts
contributions by declared order + priority, and emits byte-identical
bytes for identical inputs. The fingerprint module's `canonicalize`
helper sorts object keys at every nesting level and throws on
cycles / BigInt / function / symbol values so the canonical form
is a total function over fingerprintable payloads.

What can break it:

* A static prompt contribution that reads session state — moves
  it from fingerprint-eligible to runtime-aware.
* A custom section primitive that uses `Date.now()` or
  `Math.random()` — must be pure.

See [Prompt builder](/docs/prompt-builder) and [Prompts](/docs/prompts).

### 2. Fingerprints are pure and canonical [#2-fingerprints-are-pure-and-canonical]

`computeFingerprint` is a pure function over a typed input. The
key/metadata split rules out identity-like fields; the
canonicalization sorts object keys before hashing.

What can break it:

* Putting identity-like fields (`sessionId`, `messageId`,
  `userId`) into the input — breaks cross-session cache reuse.
* Reading `process.env` mid-compute instead of at substrate
  init — the env can drift between runs.

See [Fingerprint](/docs/fingerprint).

### 3. Stream observers are sync [#3-stream-observers-are-sync]

`onChunk` returns `Verdict` synchronously. No `Promise<Verdict>`,
no `async`. The contract is structural — the type signature
forbids the async variant.

What can break it:

* A custom observer that fires off a `fetch` and returns
  `continue` without awaiting — the work happens, but at a
  non-deterministic point relative to the next chunk.
* Observers that mutate captured state across chunks based on
  wall-clock conditions.

The legal pattern is: observe synchronously, emit a named-channel
envelope, do async work in a post-turn node. The runtime guarantees
post-turn nodes see the named envelope; the timing is
controlled by the lattice, not the observer.

See [Plugin contract](/docs/plugin-contract).

### 4. Channel reducers are commutative + associative [#4-channel-reducers-are-commutative--associative]

Concurrent writes to the same channel must produce the same result
regardless of arrival order. The built-in reducers (`appendReducer`,
`messagesReducer`, `unionReducer`) satisfy this; custom reducers
must too.

What can break it:

* A reducer that uses `Date.now()` to tie-break — wall clock
  isn't deterministic across runs.
* A reducer that reads from a global mutable state — the global
  state at replay time isn't what it was at record time.

The substrate doesn't validate the property. Violating it
silently breaks replay; tests are what catch it.

See [Channels](/docs/channels).

### 5. Identifiers carry creation order [#5-identifiers-carry-creation-order]

`recordId` on every audit row is a ULID — 26-char
Crockford-Base32 that lex-sorts to creation order. The same
ULID generator at record time produces a different id at replay
time (the timestamp embedded in the ULID is wall-clock); the
contract is that the *ordering* matches, not the values.

For replays that need the original ids, the runtime threads
`replayOfEventId` through `FingerprintMetadata` — the new ULIDs
are different, but each row carries the link to the original.

See [Audit ledger](/docs/audit-ledger).

## The fingerprint test [#the-fingerprint-test]

The single-best test for determinism: run the same turn twice
and compare fingerprints.

```typescript
import { computeFingerprint } from "@pleach/core/fingerprint";
import type { FingerprintInput } from "@pleach/core/fingerprint";

const input: FingerprintInput = {
  family:      "anthropic",
  model:       "claude-sonnet-4",
  callClass:   "synthesize",
  runtimeMode: "interactive",
  messages:    [{ role: "user", content: turnText }],
};

// computeFingerprint is pure: the same input produces the same
// Fingerprint object, byte-for-byte.
expect(computeFingerprint(input)).toEqual(computeFingerprint(input));
```

End-to-end, the recorded audit row carries the composite hash as a
string on `fingerprintComposite` — compare that field across two
runs of the same turn:

```typescript
await runtime.executeMessage(sessionA, content);
await runtime.executeMessage(sessionB, content);

const [rowA] = await ledger.getSession(sessionA);
const [rowB] = await ledger.getSession(sessionB);

expect(rowB.fingerprintComposite).toEqual(rowA.fingerprintComposite);
```

If the fingerprints diverge between runs of the "same" turn,
something in the chain has slipped. Walk back through the five
contracts above; the divergence is almost always one of:

| Symptom                          | Likely contract violated                         |
| -------------------------------- | ------------------------------------------------ |
| Prompt bytes differ              | Static contribution reading runtime state        |
| Cache misses on identical inputs | Identity-like field in fingerprint input         |
| Stream produces different chunks | Provider non-determinism (forgot to pass `seed`) |
| Channel state diverges           | Custom reducer not commutative                   |
| Audit row order shifts           | Wall-clock-tied id generation                    |

## Determinism modes [#determinism-modes]

`runtimeMode` on the runtime distinguishes five operating
contracts. The fingerprint includes it, so a turn recorded in
one mode never collides with the cache of another.

| Mode              | Behavior                                                 |
| ----------------- | -------------------------------------------------------- |
| `interactive`     | User-facing streaming chat; cache reads + writes enabled |
| `headless-eval`   | Batch eval; seed-pinned at runtime construction          |
| `headless-replay` | Cache MUST hit; a miss throws `ReplayDivergenceError`    |
| `headless-job`    | Scheduled cron / async callback                          |
| `coding-agent`    | Multi-synthesize per turn                                |

Cross-mode cache reads follow `CacheReadPolicy` — one-way only,
`interactive → headless-eval → headless-replay`. Writes are
always single-mode.

## What determinism doesn't give you [#what-determinism-doesnt-give-you]

A few things people sometimes assume but the contract doesn't
promise:

| Not promised               | Why                                                                               |
| -------------------------- | --------------------------------------------------------------------------------- |
| Cross-provider equivalence | Two providers serving the same model id can return different bytes                |
| Cross-version equivalence  | Substrate upgrades intentionally invalidate the cache via `pleachVersion`         |
| Cross-runtime equivalence  | A custom provider that drifts from the standard ones isn't bound by this contract |
| Latency equivalence        | Determinism is about output bytes, not timing                                     |

The contract is "same inputs → same outputs" within a fixed
substrate version, fixed provider, fixed model id. That's the
useful guarantee.

## Why this matters [#why-this-matters]

Three real workflows depend on the chain:

1. **Eval reruns.** A regression test that replays last week's
   production failure against this week's prompt only works if
   the bytes line up. Without determinism, every "regression"
   looks like noise.
2. **Cache correctness.** A cache that returns wrong-looking
   results is a quiet incident — the user sees the result, the
   audit row records it, but the trace doesn't reflect what
   would have happened on a fresh call. Determinism makes the
   cache sound.
3. **Compliance audit.** "Show me what the model would have
   said" is a real regulator question. The audit ledger + replay
   are the answer; both require the chain to hold.

The substrate's many "weird" decisions — sync-only observers,
quantized temperature buckets, canonicalized JSON, deterministic
reducers, ULID identifiers — are the cost of buying these three
properties.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Fingerprint" href="/docs/fingerprint" description="The hash that anchors the chain." />

  <Card title="Prompt builder" href="/docs/prompt-builder" description="`composeBudgetedPrompt` and the quantized-budget contract." />

  <Card title="Channels" href="/docs/channels" description="Reducer properties and the Channel contract." />

  <Card title="Testing" href="/docs/testing" description="Fingerprint-based golden tests." />
</Cards>
