Replay Layer¶
karenina.replay is the user-facing surface for capturing, persisting, and replaying LLM responses. The replay path short-circuits the answering call (and, when parsed_answer_fields is captured, the parsing judge) so a fully captured run replays in milliseconds. This page is the canonical reference; for the conceptual walkthrough with worked examples see Replay Store, and for how the replay store powers extend_template / extend_rubric see Extending Runs.
1. Canonical Imports¶
from karenina.replay import (
# Value types
ReplayKey,
ReplayEntry,
ReplayStore,
ReplayMissPolicy,
# Capture entry points
capture_from_result_set,
capture_from_scenario_result,
# Persistence free functions
dump,
load,
# Scenario projection
ScenarioReplayBuilder,
ProjectionReport,
OrphanEntry,
UnmatchedTarget,
# Exceptions
ProjectionError,
ReplayMissError,
ReplayHydrationError,
ReplayPersistenceError,
ReplayError,
)
from karenina.replay.ports_message_hydration import hydrate_trace_messages
2. Value Types¶
2.1 ReplayKey¶
Frozen Pydantic v2 model identifying where a captured turn lives in the evaluation graph. Two keys with the same field values are equal and hashable, which is what makes the lookup index O(1).
| Field | Type | Description |
|---|---|---|
question_id |
str |
Question id (URN, derived from question text via generate_question_id). Required. |
scenario_id |
str \| None |
When set, the key matches a turn inside a scenario; lookup is keyed by (scenario_id, scenario_node) and question_id is informational. |
scenario_node |
str \| None |
Node id inside the scenario; used together with scenario_id. |
answering_model_id |
str \| None |
Canonical model display string ("interface:model_name"). None is a wildcard matching any answering model. |
visit_index |
int \| None |
Zero-based count of how many times the same node was visited inside one scenario run. None matches any visit. |
replicate |
int \| None |
Replicate index. None is a wildcard matching any replicate. |
Lookups walk a four-step specificity ladder over (answering_model_id, visit_index, replicate): most-specific first, wildcards last.
2.2 ReplayEntry¶
The captured payload itself. Pydantic v2 with extra="forbid".
| Field | Type | Description |
|---|---|---|
raw_trace |
str |
Captured response text. Always present. May be empty. |
parsed_answer_fields |
dict \| None |
Pre-parsed Answer fields. When present, the parsing judge is bypassed entirely. |
trace_messages |
list[dict] \| None |
Captured agent trace as Message.to_dict() outputs. Rehydrated via hydrate_trace_messages before being threaded into downstream stages. |
verify_result |
bool \| None |
Captured template verdict. Enables verdict-only replay for rows whose raw trace cannot be reparsed. |
usage_metadata |
dict \| None |
Captured usage summary, rehydrated into the replayed result so token accounting survives replay. |
agent_metrics |
dict \| None |
Per-turn agent telemetry. The replay path only consumes limit_reached, which propagates to recursion_limit_reached and feeds the recursion-limit autofail stage. |
captured_model_id |
str \| None |
Canonical display string of the model that produced the entry. Provenance only. |
captured_at |
str \| None |
ISO timestamp of capture. Provenance only. |
2.3 ReplayMissPolicy¶
fall_through (default): a miss returns None from lookup() and the live pipeline takes over. strict: a miss raises ReplayMissError. Use strict for frozen captures in CI and fall_through for hybrid replay-plus-live runs.
3. ReplayStore¶
Keyed store of (key, entry) pairs with specificity-based lookup. The entries list is the source of truth; internal scenario / QA indexes are rebuilt from entries after every mutation.
3.1 Constructors¶
| Symbol | Signature | Purpose |
|---|---|---|
ReplayStore |
(*, miss_policy: ReplayMissPolicy = "fall_through", entries: list[tuple[ReplayKey, ReplayEntry]] = []) |
Default constructor. |
ReplayStore.load (classmethod) |
(path: str \| Path, *, miss_policy: ReplayMissPolicy \| None = None) -> ReplayStore |
Load from JSON; thin wrapper around karenina.replay.persistence.load. |
ReplayStore.from_manual_traces (classmethod) |
(manual_traces: Any, benchmark: Any, *, miss_policy: ReplayMissPolicy = "strict") -> ReplayStore |
Build from a legacy ManualTraces instance. Emits one wildcard entry per registered trace (answering_model_id=None, visit_index=None, replicate=None). The classmethod Benchmark.run_verification uses internally when interface="manual" is in play. |
3.2 Mutation¶
| Method | Signature | Behavior |
|---|---|---|
register |
(key: ReplayKey, entry: ReplayEntry) -> None |
Add or overwrite an entry. Overwrites emit a WARNING log. Not safe to call concurrently with pipeline execution. |
3.3 Lookup¶
| Method | Signature | Behavior |
|---|---|---|
lookup |
(*, question_id: str, scenario_id: str \| None = None, scenario_node: str \| None = None, answering_model_id: str \| None = None, visit_index: int \| None = None, replicate: int \| None = None) -> ReplayEntry \| None |
Walk the specificity ladder and return the matching entry, or None (or raise ReplayMissError when miss_policy="strict"). |
has_any_for |
(*, question_id: str, scenario_id: str \| None = None, scenario_node: str \| None = None) -> bool |
Cheap outer-key probe; does NOT walk the ladder. |
3.4 Persistence¶
| Method | Signature | Behavior |
|---|---|---|
save |
(path: str \| Path) -> None |
Serialize to JSON; thin wrapper around dump. |
The JSON file is a versioned wrapper around a list of (key, entry) pairs emitted in store order (the order in which they were registered). Diff-friendliness comes from json.dump(..., sort_keys=True, indent=2), which sorts the keys inside each JSON object and pretty-prints, not from sorting the entries list by key. Writes go through tempfile plus os.replace rename, so partial writes never reach the visible file.
4. Capture Entry Points¶
4.1 capture_from_result_set¶
capture_from_result_set(
result_set: VerificationResultSet,
*,
include_parsed: bool = True,
include_agent_traces: bool = True,
only_successful: bool = True,
answering_model_ids: set[str] | None = None,
scenario_ids: set[str] | None = None,
replicate_selector: Literal["all", "first", "last"] = "all",
) -> ReplayStore
Walk a VerificationResultSet and emit a populated store. The default emits one entry per successful turn with parsed fields and structured trace messages included.
| Argument | Effect |
|---|---|
include_parsed |
When True, store parsed_answer_fields so the parsing judge is bypassed on replay. |
include_agent_traces |
When True, store the structured trace_messages. Set to False to drop tool-call detail and reduce file size. |
only_successful |
When True, drop turns whose pipeline did not produce a verifiable output (retry exhaustion, autofail, abstention, parsing or system errors). Content failures (clean pipeline, verify() returned False) are retained. |
answering_model_ids |
Allow-list by canonical model display string. |
scenario_ids |
Allow-list by scenario id. Applies only to scenario turns; QA turns pass through unaffected. |
replicate_selector |
"all" preserves replicate keys; "first" and "last" collapse to replicate=None wildcards (required by ScenarioReplayBuilder). |
VerificationResultSet.to_replay_store(**kwargs) is a forwarding wrapper around this function.
4.2 capture_from_scenario_result¶
capture_from_scenario_result(
scenario_result: ScenarioExecutionResult,
*,
answering_model_id: str,
scenario_id: str | None = None,
nodes: set[str] | None = None,
include_parsed: bool = True,
include_agent_traces: bool = True,
replicate: int | None = None,
) -> ReplayStore
Walk a single ScenarioExecutionResult. answering_model_id is required because a ScenarioExecutionResult does not carry per-turn model identity. scenario_id defaults to scenario_result.scenario_id when not supplied. nodes is an optional allow-list of node ids.
5. Persistence Free Functions¶
dump(store: ReplayStore, path: str | Path) -> None
load(path: str | Path, *, miss_policy: ReplayMissPolicy | None = None) -> ReplayStore
ReplayStore.save and ReplayStore.load are thin wrappers around these. Both routes are bit-for-bit equivalent. The methods are the canonical entry points; the free functions exist for symmetry and discoverability.
6. Scenario Projection¶
ScenarioReplayBuilder projects QA-mode ReplayStore entries onto scenario-mode keys, turning one (question, model) QA run into N scenario-mode replay entries that serve a shared "ask" turn across many dialogue framings.
6.1 ScenarioReplayBuilder¶
| Method | Signature | Behavior |
|---|---|---|
__init__ |
(benchmark: Benchmark, *, config: VerificationConfig, miss_policy: ReplayMissPolicy = "strict") |
Bind the builder to a benchmark and a default config (used for projections that omit config=). miss_policy is written onto the produced ReplayStore, so the default raises ReplayMissError on a miss rather than falling through. |
add_qa |
(qa_store: ReplayStore, *, target_nodes: list[str], scenarios: list[str] \| None = None, config: VerificationConfig \| None = None) -> ScenarioReplayBuilder |
Stage a projection and return self for chaining. Requires qa_store captured with replicate_selector="first" or "last" (which emit replicate=None); "all" is rejected. |
validate |
() -> ProjectionReport |
Walk all staged projections without mutation. |
build |
(*, strict: bool = False) -> ReplayStore |
Run validate, then register matched entries into a fresh store. strict=True raises ProjectionError on any unmatched or duplicate target. strict=False emits a warning and returns whatever it matched. |
6.2 ProjectionReport¶
| Field | Type | Description |
|---|---|---|
projected_keys |
list[ReplayKey] |
One per resolved (scenario, node) target. |
unmatched_targets |
list[UnmatchedTarget] |
Targets that did not resolve to a QA entry. |
orphan_qa_entries |
list[OrphanEntry] |
QA entries no staged projection consumed. |
duplicate_targets |
list[tuple[str, str]] |
(scenario_id, node_id) pairs hit by more than one projection. build(strict=False) applies last-projection-wins. |
matched (property) |
int |
len(projected_keys). |
6.3 UnmatchedTarget¶
| Field | Type | Description |
|---|---|---|
scenario_id |
str |
Scenario name that was scanned. |
node_id |
str |
Node id requested as a projection target. |
question_id |
str \| None |
The node's question id when resolution reached that step. |
answering_model_id |
str \| None |
Effective runtime answering model id at the target. |
reason |
Literal["missing_scenario", "missing_node", "no_qa_entry"] |
Why resolution failed. |
6.4 OrphanEntry¶
| Field | Type | Description |
|---|---|---|
question_id |
str |
Question id of the orphaned QA entry. |
answering_model_id |
str \| None |
Captured ReplayKey.answering_model_id. |
reason |
Literal["no_target_scenario", "model_id_never_requested"] |
no_target_scenario: question_id not attached to any node in any of the projection's declared scenarios. model_id_never_requested: question matched at least one targeted scenario / node, but the effective runtime model at every such target differed from the entry's answering_model_id. |
7. Exception Hierarchy¶
All replay-layer exceptions inherit from karenina.replay.ReplayError, which inherits from karenina.exceptions.KareninaError.
| Exception | Raised by | Carries |
|---|---|---|
ReplayError |
Base class | nothing |
ReplayMissError |
ReplayStore.lookup under miss_policy="strict" |
key: ReplayKey |
ReplayHydrationError |
parse-bypass when parsed_answer_fields does not validate against the current Answer class under replay_parse_on_hydration_mismatch="strict" |
captured_fields: dict, inner: Exception |
ReplayPersistenceError |
load for missing file, malformed JSON, version mismatch, schema error, or invalid miss_policy string |
the offending detail in the message |
ProjectionError |
ScenarioReplayBuilder.build(strict=True) on any unmatched or duplicate target |
the ProjectionReport |
8. Helpers¶
8.1 hydrate_trace_messages¶
Rehydrate captured trace_messages dicts into port Message objects. Used internally by the replay short-circuit; documented here for completeness because it is the seam where captured agent traces become indistinguishable from live ones from the perspective of every stage after GenerateAnswer.
9. Cross-References¶
- Replay Store: conceptual walkthrough with worked examples.
- Extending Runs: how
extend_template/extend_rubricbuild aReplayStorefrom prior results. - Sinks reference: how
ProgressiveFileSinkinteracts with replay-driven extension runs. - Scenarios: Execution:
ScenarioExecutionResult.to_replay_storefor multi-turn captures.