Skip to main content

Controlling evaluation cost

The three-tier ladder is itself the primary cost control: the expensive metrics only run when the cheap ones say something is wrong. Tier 1 is deliberately cheap — coherence is embedding-based rather than an LLM judge — so the always-on rung is the least expensive one. Tier 3 is opt-in because every LLM-judge metric reads the whole trace, so cost scales with metric count.

Controlling repair cost

The repair agent is the loop’s second model consumer, and it only runs when a notice was posted — so the tier ladder gates it too. It is bounded on four axes: RepairResult.usage reports normalized input/output tokens and cost whenever the provider supplies them, so repair spend is measurable per episode rather than inferred. Repair also makes at most one episode per settled turn.
Point repair_model at a cheaper model than your task model if diagnosis quality allows. It reads bounded evidence and writes one short rule — a smaller model is often sufficient, and the setting is independent of whatever your agent runs on.
Validation is the third consumer, and the least predictable: each replay re-runs your agent. validation_round_budget_s bounds replay work per round, and regression_sample bounds a regression run. Four independent dials bound the spend on top of that: Two structural savers:
  • Supersede — a newer turn cancels the session’s in-flight evaluation; you never pay for a stale turn’s scores.
  • One batched run per turn — Tier 1 covers every new trace in a single create + poll, not one run per trace.
eval_sample_every > 1 bounds cost by skipping turns, which thins the series the trajectory gate needs. It is a legitimate dial, but it trades away detection sensitivity in a way the others don’t. Reach for enable_tier3=false and max_evals_per_run first.
Replay costs are yours (a replay re-runs your agent), which is why validation replays at most a handful of cases per candidate, regression runs are sequential and sampleable (regression_sample), and every replay invocation is time-bounded (replay_timeout_s).

Latency and the barrier

The barrier is the one place the harness deliberately makes your loop wait. It buys in-session healing; the cost is per-turn latency bounded by barrier_timeout_s (default 180s).
If per-turn latency is unacceptable in your product but you still want in-session healing, settle on a subset of turns — every Nth turn, or only after a tool-heavy turn. The gate still gets its series from Tier 1 on every turn; the barrier only controls when the agent finds out.

Degradation ladder

The harness’s core invariant: nothing on the harness’s side may break or stall your agent. Every failure mode has a defined, observable degradation:

Concurrency model

  • on_turn_end is synchronous and cheap: parse, gate, schedule — it returns before any I/O.
  • Evaluations run as detached tasks under a global semaphore; candidate validation is a single-flight background round, bounded by validation_round_budget_s with forward-trial fallback for candidates replay cannot reach; blocking file I/O runs on the thread pool.
  • The trajectory gate’s fold is history-dependent, so a whole turn’s traces are folded in order — in one thread hop and one history write per trace, not one per metric.
  • All workspace stores are lock-guarded with atomic writes (unique temp file + rename) and append-only logs — one workspace safely serves many concurrent sessions, and readers never observe a half-written file.
  • Per-session bookkeeping is bounded (a few thousand sessions) with oldest-first eviction, so long-lived processes don’t grow without limit. Evicting a session also drops its trace seen-set.
refresh(session_id), refresh_all(), and drain_validation(timeout=...) are bounded joins (drain_timeout_s by default; drain_validation returns whether it finished, and settle_validation(timeout=...) is the phase-boundary call) for tests and explicit callers — correctness never depends on calling them. settle() is the one wait that is part of the design; hook.pending_sessions is the read-only view for host-side phase barriers.

Scaling out

Score history is local by default, which means a session handled by two replicas has two partial series. The history source is a small Protocol, so a shared remote store can replace the local JSON file without touching anything else — that is the supported fix for replica fan-out.
The gate’s state (running peak, stall counter) lives in the same state/score_history.json entry as the score series itself — no second file, no second lock — so it inherits whatever durability that store has.
For the workspace itself, give each replica its own HARNESS_ROOT or mount a shared volume: all stores are multi-session safe within a process, and cross-process safety rests on atomic renames and append-only files.

Operating recommendations

  1. Settle per turn, not per task. This is the one integration mistake that silently disables detection — the gate needs a series. See the barrier.
  2. Start in shadow mode (observe_only=true) and calibrate your Tier-2 thresholds and gate_target against real traffic.
  3. Tune gate_window to your task horizon — long-horizon agents need a wider window than short interactive turns.
  4. Wire an outcome verifier if you have any ground truth at all. It is the strongest promotion signal available, and it costs nothing per turn.
  5. Turn on capture (capture_eval_cases=true) so the closed loop has scenarios to replay, and curate a few protected win cases.
  6. Wire a replay function — it upgrades rule validation from statistical to counterfactual and unlocks regression runs.
  7. Schedule pandaprobe-harness-eval (nightly, or after prompt changes) and alert on a non-zero exit.
  8. Call settle_validation() at phase boundaries — before archiving a workspace, snapshotting a ruleset, or reporting results. Waiting on evaluations alone can freeze a candidate that had already earned promotion as permanently provisional.
  9. Watch the journalrule_promote / rule_retire tell you what is being learned; validation_verdict’s pending_reason tells you why something is not decided yet; needs_human notices tell you when to step in.