Runtime & Integrations · Evaluation Core

Unit ladder

The unit: route key that grades a whole run. next

A traces route declares unit: call | step | trajectory | session (a UnitKind) to choose the slice it grades. call is one model call; a richer kind groups every unit sharing a trace_id into one aggregate scored by trajectory_shape@1. Absent unit: is byte-identical to call. Built in the framework; not yet in the version the plugin installs.

This is one route through the existing TraceEnvelope → EvalUnit → Example spine — no new score meaning and no second verdict path. The human-first walkthrough is Trajectory & session grading; this page is the typed contract.

The route key

TraceConfig carries a kind: UnitKind (YAML key unit), parsed with the same _coerce_enum pattern as format / data_policy. The key is additive and fail-closed: absent ⇒ call (every pre-P1 config unchanged), and a present-but-bogus value is a setup error, not a silent fallback.

unitGradesSelection
callOne model call — its input and output.The default. One Example per call; the run fingerprint is byte-identical to a config with no unit:.
stepCalls sharing a trace_id.Grouped by trace_id like trajectory (finer step locators are out of scope).
trajectoryAn ordered run — all calls sharing a trace_id.Grouped into one aggregate Example with unit_id = "trajectory:<trace_id>".
sessionA whole session.Grouped the same way, unit_id = "session:<trace_id>".

Reachability is two-part: the key parses at the config layer, and trajectory_shape is registered in the harness evaluator loader so a metric may declare evaluator_ref: trajectory_shape@1. The TRACE_SOURCE optional lane stays call-only — aggregate grading is reachable through the built-in traces: route.

How selection works

select_units(units, kind=…) is the selector. CALL delegates to the unchanged per-call path; a richer kind groups call-level TraceUnits by trace_id (insertion order preserved) into one aggregate Example whose output is the per-member output list and whose unit.members lists the sub-unit ids. trajectory_shape@1 reads that members list and reports the fraction that produced a non-null output — one shape signal, never a claim the agent succeeded. The same trace_id split across two trace files yields two aggregates (grouping is per-read).

Worst-of-members egress

Collapsing N members into one aggregate drops the per-unit egress bool the runner relies on. _load_trace_units(units, kind) resolves it: the aggregate’s egress is the worst of its members — permitted iff every member is, so a single forbidden / unknown / missing member blocks the whole aggregate (fail-closed). A forbidden trace can never leak into a replay or judge egress through an average of the permitted parts.

The honesty guards

Two guards, both fail closed trajectory_shape returns non_evaluablenot_aggregate on a call unit (a metric whose granularity disagrees with the produced unit guards itself), no_members on an aggregate with no sub-units, and output_all_null when no member produced an output. That last case is the empty-trajectory rule: 0/N = 0.0 would read as “0% complete quality” when the honest state is “no evidence”, so it is non_evaluable, never a fabricated 0.0.

And it cannot gate. Traces carry no validated gold, so an aggregate run inherits proposed status and resolves informational. An approved gating threshold over proposed trace data does not activate a gate — it stays informational, never a laundered pass.

An illustrative aggregate

One aggregate row for a three-member trajectory where two members produced output:

json

{
  "unit_id": "trajectory:trace-42",
  "metric": "trajectory_shape",
  "point": 0.67,
  "verdict": "informational",
  "status_counts": { "scored": 1 }
}

Illustrative example, not a measured result. The shape is 0.67 — a description of the recorded run, not a pass and not proof the agent succeeded.

Next steps