Serves language-model responses from a frozen fixture file.
Evaluation needs a workflow LLM whose answers do not move between a baseline run and a candidate run, otherwise a behavioural difference cannot be attributed to the candidate. This is an installed host source rather than an Elixir test callback so an evaluation recipe stays ordinary configuration: the same manifest grammar selects a replay provider or a live one, and nothing about the application changes between them.
A fixture file is JSON Lines. Every entry requires "schema_version": 1, a
request_hash matching sha256: followed by 64 lowercase hexadecimal
characters, and exactly one of response or responses. response is one
JSON object. responses is an ordered, non-empty sequence of at most 1,024
JSON objects. No other entry keys are accepted.
For example, one line is:
{"schema_version":1,"request_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","response":{"content":"frozen"}}The sequence form exists for a request that repeats identically — a retry, or a loop that rebuilds the same prompt — where the first call gets the first element and the next call the next. An ordinary multi-turn agent loop does not need it: each turn carries the accumulated transcript, so each request hashes differently and gets its own entry.
The hash is over the deterministic encoding of the provider-neutral request
the workflow actually built, before any provider adapter sees it, so a
fixture is not tied to the vendor that recorded it. That also makes the match
exact by construction: a run whose prompt, messages, or tools differ at all
produces a different hash and fails rather than silently replaying a response
recorded for a different question. A normal-data miss reports the computed
hash as
no replay fixture matches this request (request_hash: sha256:...) in command
output and private capability inspection. A private-data run keeps the hash
out of its public command diagnostic because the unsalted request hash could
reveal equality or permit guesses of low-entropy prompts; author those
fixtures from the owner-only inspection record instead. Copy the hash into
the entry, provide the response the workflow expects, and rerun.
The provider is owned. Its response cursor lives in a process that monitors the run that acquired it, so a run failing between acquisition and cleanup cannot leave a replay owner behind.
Every failure is closed. An unknown hash, an exhausted sequence, a malformed or oversized response, a duplicate entry, or a fixture past its ceilings all fail the call rather than inventing or reusing a response. Nothing here performs network activity, and the safe snapshot carries the format version, fixture-set hash, entry counts, and ceilings — never a payload or a path.
Load failures name the rule that refused the file. A rejected line reports
{reason, line}, and the line number is the number in the file, blank lines
counted, so a fixture author can open the file at the offending line. The
reason and the number are the only per-line detail that crosses the boundary;
the line's bytes never do.
Summary
Functions
Validates one fixture file without starting its response-cursor owner.
Hashes a provider-neutral request the way fixture keys are computed.
Returns the requester LLMCapability calls, so replay and live installations
present the identical provider-facing contract.
Safe provider identity. Carries counts and ceilings, never payloads or paths.
Loads one fixture file and starts the owner that tracks sequence position.
Stops the owner. Safe to call more than once.
Types
@type entry_reason() ::
:invalid_json
| :entry_not_an_object
| :unknown_entry_key
| :schema_version_invalid
| :request_hash_invalid
| :response_missing
| :response_ambiguous
| :responses_invalid
| :response_too_large
| :duplicate_entry
| :entry_limit_exceeded
@type error() :: :replay_fixtures_unreadable | :replay_fixtures_empty | :replay_fixtures_too_large | :replay_owner_unavailable | :resource_registrar_unavailable | {entry_reason(), pos_integer()}
@type fixture_summary() :: %{ entry_count: pos_integer(), response_count: pos_integer(), fixture_hash: binary(), max_result_bytes: pos_integer() }
@type t() :: %PtcRunner.Kernel.LLMReplay{ entry_count: non_neg_integer(), fixture_hash: binary(), max_result_bytes: pos_integer(), pid: pid(), response_count: non_neg_integer() }
Functions
@spec probe(binary(), binary(), keyword()) :: {:ok, fixture_summary()} | {:error, error()}
Validates one fixture file without starting its response-cursor owner.
Doctor uses this bounded, process-free probe before provider activity. It
reads and parses the same bytes under the same ceilings as start/3, so a
passing local check cannot disagree with acquisition about fixture validity.
Hashes a provider-neutral request the way fixture keys are computed.
Manifest authors normally copy the hash from a replay-miss command error; this function supports embedders that already hold the provider-neutral request map.
@spec requester(t()) :: (map() -> {:ok, map()} | {:error, PtcRunner.Kernel.ProviderError.t()})
Returns the requester LLMCapability calls, so replay and live installations
present the identical provider-facing contract.
Safe provider identity. Carries counts and ceilings, never payloads or paths.
Loads one fixture file and starts the owner that tracks sequence position.
directory is the trusted host-config directory the relative path is
confined to. max_entries bounds distinct request hashes and
max_result_bytes bounds one replayed response.
@spec stop(t()) :: :ok
Stops the owner. Safe to call more than once.