Saved Cases
A case is a recorded run frozen at a fork point: inputs, history up to the fork, every recorded call result. It is a content-hashed bundle, so saving the same run at the same fork twice gives one copy.
export PYRULA_FIXTURE_STORE=/srv/pyrula/casespyrula case save my_workflow run_abc123 --fork-after-seq 42{"fixture_hash": "9f2c...", "fixture_store": "file:///srv/pyrula/cases", "manifest_summary": {"format": 1, "kind": "workflow", "target_name": "my_workflow", "fork_after_seq": 42, "capture": {"complete": true, "missing": []}, "result_count": 6, "history_count": 19}}--fork-after-seq is a run_seq, not an event count. Omit it to keep only the inputs.
Fixture stores
Section titled “Fixture stores”Resolved from --fixture-store, then PYRULA_FIXTURE_STORE, then PYRULA_POSTGRES_DSN. With none set, every subcommand refuses with no_fixture_store.
| Value | Store |
|---|---|
memory: | Per instance, tests only. |
file:///abs/path or a bare path | A local directory, created if missing. |
postgres://... | Tables in the archive schema. Run pyrula db migrate first. |
s3://bucket/prefix?region=...&endpoint_url=... | Needs the "pyrula[workflows,s3]" extra. Credentials come from the AWS SDK chain, never the URL. |
Cloud workers
Section titled “Cloud workers”Cloud-connected workers read the same PYRULA_FIXTURE_STORE / PYRULA_POSTGRES_DSN. Ordinary workers support save_case, run_case and delete_case_bundle; Kafka workers and API-only processes don’t.
A run_case message names a source:
bundle: a saved case from the fixture store. Needs a store.journal: a run on the worker’s own journal, archived runs included. No store needed, but thensave_caseanddelete_case_bundleare unavailable.
If the run aged out of Valkey (completed_ttl, a day by default) with no archive, the new run ends whatif_source_missing; a changed rebuild ends whatif_source_changed. Budgets, labels and codes: What-if runs. A run_case ack means only that the run was durably submitted; the outcome arrives in the run’s events.
Save, export, import
Section titled “Save, export, import”pyrula case save TARGET RUN_ID reads the run from Valkey (--namespace) or the Postgres archive and writes the bundle to the store. It refuses a capture replay could not use (capture_incomplete). --kind is detected from the run’s model and tool steps; --kind workflow on a run with model or tool calls refuses with kind_mismatch.
pyrula case export 9f2c... --out run.json --json # readable, diffablepyrula case import run.json # same hash backpyrula case listpyrula case show 9f2c... [--values]pyrula case delete 9f2c...export writes the compressed blob by default; --json gives a diffable file. import accepts either.
Bundle format
Section titled “Bundle format”Bundles hold raw inputs and outputs, unredacted. Treat a fixture store like a copy of the data the run touched; export and show --values warn on stderr.
Format 1 is JSON with three keys:
manifest:format,kind,target_name,code_version,registration,recorded_config,pyrula_version,source,fork_after_seq,captureandinputs.history: events withrun_seq <= fork_after_seq, terminal kinds dropped.results: every recorded call result, including after the fork, as{key, kind, fingerprint, result}or{..., error}.
The hash is SHA-256 of the uncompressed bytes (sorted keys, compact separators, UTF-8, results sorted by (kind, key)). An unknown manifest kind or unreadable format refuses with format_unsupported. Bundles over 512 MiB are refused at save.
For agents, registration.tools lists every tool offered with its writes flag, and recorded_config.model is the turn’s first model call.
Cases in version control
Section titled “Cases in version control”pyrula case save my_workflow run_abc123 --fork-after-seq 42 --fixture-store tests/casespyrula case export 9f2c... --out tests/cases/9f2c....json --jsonGarbage collection
Section titled “Garbage collection”pyrula case gc --keep 9f2c... --keep-file keep-hashes.txt [--yes]Without --keep every bundle is a candidate. Without --yes, gc only lists.
Running a case
Section titled “Running a case”pyrula case run starts a local run from a bundle, named by its hash in the fixture store or by a .case file such as the ones pyrula dev keeps in .pyrula/last/. It first replays the recorded prefix in verification mode: every prefix call must be consumed with its recorded identity and fingerprint, or it refuses with prefix_diverged.
| Flag | Effect |
|---|---|
--from-start | Reruns the whole recorded run instead of only what follows the fork. |
--model, --prompt-file | Run an agent’s model calls live on another model or system prompt (Record once, iterate free). |
--input KEY=JSON | Changes an input; only for inputs that don’t change the prefix. |
Each call then resolves down a ladder:
- Reuse the recorded result (
reused). - Use a supplied answer (
simulated). - Run live if permitted (
livefor a new input,changedwhen it replaces a recorded result). - Stop with a reason code.
pyrula case run CASE_HASH --target my_app.workflows:checkout \ --fixture-store tests/cases --input sku='"SKU-42"'Agent cases also need --llm-ref MODULE:ATTR. --deps-ref MODULE:ATTR supplies dependencies, --app-dir DIR extends the import path, --run-id ID sets the run id.
Live calls and effects
Section titled “Live calls and effects”New model and read calls run live by default, within --live-budget N (max_live_calls). Writes stop with write_not_allowed unless allowed with --allow-write TOKEN, repeated per token. Steps declare effect="read" | "write" | "pure"; undeclared is "write". A tool is a read only with @tool(read_only=True) or as a sub-agent. Pure calls do not count against the budget.
Tokens: tool:<name> for a tool the model calls, step:<name> for one the agent body calls, and step:<id> using ids from registration.steps (step:child:0, step:sigsend:0, step:gather:<id>). A signal send is a write.
Replay only
Section titled “Replay only”--replay-only removes the live rung: every non-pure call must be answered by the recording, so no model client is needed and --supply does not apply. An unanswerable call stops with replay_diverged, with reason one of input_changed, unexpected_call or recorded_error. A recorded post-fork call the run never makes is listed in unconsumed_keys and the command exits 3.
A pass means the changed code made the same calls with the same inputs, not that a live model would answer the same. Replay checks in CI runs a Cloud suite this way.
Supplied results and budgets
Section titled “Supplied results and budgets”--supply FILE is a JSON object with calls (keyed "<kind>:<key>") and tools (keyed by tool name), at most 200 entries each and 64 KiB total. A wrong-shaped answer stops with missing_context. Model answers are a string or {"role": "assistant", "content": [...]}; interrupt answers are {"action": "...", "value": ...}; signal answers require payload.
--max-cost-usd X limits spend using the default price table. --prices FILE adds or overrides models (at most 100, cache rates default to the input rate; not stored in the bundle). An entry may add "long_context": {"above_input_tokens": N, "input_per_mtok": ..., "output_per_mtok": ...} (cache rates optional, defaulting to that tier’s input rate): a call whose prompt (input + cache creation + cache read tokens) is above N is priced entirely at the tier’s rates:
{"model-id": {"input_per_mtok": 2.5, "output_per_mtok": 10, "cache_write_per_mtok": 2.5, "cache_read_per_mtok": 0.5}}Under a cost limit, a live model call with no price or unobservable spend stops with live_budget_exceeded.
Waits and tool ids
Section titled “Waits and tool ids”A case run never waits. A post-fork durable sleep advances the run’s clock (and ctx.now()) to its wake time. Interrupt, signal and external stream reads with no recorded or supplied answer stop with missing_context.
A live model call gets new tool-use ids, so recorded tool results keyed by the old ids won’t match. Supply them under tools, keyed by tool name.
Output and exit status
Section titled “Output and exit status”--values includes call values. The command prints one {"type":"call", ...} line per new post-fork call, then a {"type":"outcome", ...} line with status, provenance counts, changed_calls, live-call count, cost, code versions and fixture hash. Exit 0 is complete, 2 a refusal before start (JSON code and message on stdout), 3 a started run that failed, was cancelled or suspended, 1 invalid input or operational error (redacted error: on stderr).
Which call stopped a run
Section titled “Which call stopped a run”A missing_context, live_budget_exceeded, write_not_allowed or replay_diverged stop puts stop_key (such as tool:toolu_01, the key supplied_results.calls uses) and stop_token (the --allow-write token for a refused write, else null) on the run:error event.
Reason codes
Section titled “Reason codes”Refusals exit 2 with {"code", "message"} on stdout; other failures exit 1.
| Code | Meaning |
|---|---|
capture_incomplete | The message lists what is missing, including a call in flight at the fork. Pick another --fork-after-seq or save after the run finishes. |
no_fixture_store, bundle_missing, bundle_corrupt | No store configured; hash not in the store; bytes fail decompression, hashing or validation (re-export). |
format_unsupported, kind_mismatch | Bundle format not readable by this version; --kind workflow on a run with model or tool calls. |
invalid_fixture_hash, invalid_keep_hash | Not a 64-character lowercase hex hash. |
prefix_diverged | The target no longer consumes the recorded prefix with matching inputs. |
replay_input_changed | Resuming finds changed input in the case run’s own journal. |
missing_context | A call has no live rung, or a supplied answer has the wrong shape. |
live_budget_exceeded | Call or cost ceiling reached, or spend unobservable under a cost limit. |
write_not_allowed | A write has no allow token. |
replay_diverged | A --replay-only call the recording cannot answer. |
binding_mismatch, invalid_overrides | Expected fixture store identity differs from the opened store; runtime settings missing or invalid. |
whatif_source_missing, whatif_source_changed | See What-if runs. |
runtime_error, cancelled | The runtime failed, or the run was cancelled. |
no_eligible_runtime, runtime_lost, runs_quota_exceeded | Cloud only. |