Configure capture two ways: environment variables (the gate and the output root) and the programmatic configure() call (sinks, sampling, dedup, GC). Everything here is fail-open — a config or hygiene fault drops a capture, never the host agent.
## Environment
EVALSHIFT_CAPTURE=1 # gate: on for 1/true/yes/on, off otherwise (default off) EVALSHIFT_DIR=/var/captures # capture root; default is ./.evalshift EVALSHIFT_MAX_CAPTURES=200 # newest-N cap per suite dir (default 200; 0/off = unlimited) EVALSHIFT_CAPTURE_TTL=604800 # evict captures older than N seconds (default off) EVALSHIFT_DEDUP=on # collapse identical-input captures (default on) EVALSHIFT_SAMPLE_RATE=0.1 # capture this fraction of runs (default off = capture all)
- +
EVALSHIFT_CAPTURE— the off-by-default gate. Truthy values:1/true/yes/on. - +
EVALSHIFT_DIR— the capture root. Captures land at<base>/captures/<suite>/cap_<id>.json; toolset sidecars at<base>/toolsets/<hex>.json, written once per distinct toolset. - +
EVALSHIFT_MAX_CAPTURES,EVALSHIFT_CAPTURE_TTL,EVALSHIFT_DEDUP,EVALSHIFT_SAMPLE_RATE— the hygiene knobs, for hosts that cannot callconfigure(). For the numeric ones,0/none/unlimited/offmeans “disabled”.
Precedence is configure(...) > environment variable > built-in default, and a malformed value fails open to the default — a bad env var can never crash your agent. The gate and EVALSHIFT_DIR are read live (per call, per write); the hygiene variables are read once at config construction (import or reset_config()).
## configure()
configure() has merge semantics — only the keyword args you pass change. reset_config() restores defaults (useful in tests). There is no redact knob — masking is required at each capture point, not set process-wide. See Redaction.
from evalshift import configure, MemorySink
configure(
sink=MemorySink(), # where captures go (default: FileSink)
sample_rate=0.1, # capture 10% of runs (default: capture all)
dedup=True, # collapse identical-input captures (default: True)
max_captures=500, # cap per-suite file count (default: 200)
capture_ttl=86400, # evict captures older than 1 day (default: off)
require_model_call=True, # drop captures with no model_call span (default: False)
)| Option | Effect |
|---|---|
sink | Where captures are written (default FileSink). |
sample_rate | Fraction of runs to capture, decided at agent entry. |
dedup | Collapse captures with identical input (per process). |
max_captures | Cap a suite directory by file count; evict oldest by mtime. |
capture_ttl | Evict captures older than this many seconds. |
require_model_call | Drop captures that recorded no model_call span (default off). |
## Sinks
- +FileSink (default) — one JSON file per capture. The root resolves at write time: an explicit
FileSink(base=...)wins, elseEVALSHIFT_DIR, else.evalshiftrelative to the working directory. No repo-root walk — the SDK never assumes it runs inside a checkout. On a filesystem error (read-only mount, disk full) it drops the capture, logs at debug, and returns — it does not crash. - +MemorySink — keeps captures in memory for ephemeral hosts (Lambda, tests); you drain them yourself.
from evalshift import configure, MemorySink sink = MemorySink() configure(sink=sink) # ... run agents (zero disk writes) ... captures = sink.flush() # drain everything captured
## Hygiene
Capture can be a firehose, so it self-manages — and two of the three mechanisms are on by default, so a host that enables the gate and never calls configure() still cannot grow .evalshift/captures/ without limit. Whenever any knob is active the sink is transparently wrapped; with all three disabled your sink is used bare.
- +Sampling (
sample_rate) — decided once at agent entry.0.0captures nothing;1.0/ unset captures every run. A fault in the draw fails open (captures). - +Dedup (
dedup, on by default) — per-process, best-effort collapse of identical-input captures, keyed on(suite, input_hash). The registry resets with the process. - +GC (
max_captures, 200 per suite by default, andcapture_ttl, off) — evicts the oldest captures by filesystem mtime once a suite exceeds the cap or ages past the TTL. GC only ever scans<base>/captures/<suite>/— toolset sidecars under<base>/toolsets/are never touched (the CLI prunes those, refcounted, viacapture clean).
# opt back out of hygiene entirely (write every capture, keep them all) EVALSHIFT_MAX_CAPTURES=0 EVALSHIFT_DEDUP=off
One more persistence gate, off by default: configure(require_model_call=True) drops captures that recorded no model_call event. A capture with no model output carries nothing scoreable, so eval-grade hosts that only want promotable captures turn it on.
