6.7 KiB
6.7 KiB
T5.11 — Degradation reasons
| Field | Value |
|---|---|
| Phase | P5 — Grading |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
Goal
When no comparison is possible, emit Score::Ungraded { reason } — a stated
reason, never a neutral score — and reach terminal Ungraded.
Facts (inlined — no spec read needed)
Three reasons, each a distinct condition:
NoReference— a novelTaskId. Nothing else has done this task, so there is nothing to compare against. That is not a degradation to engineer around; it is the first observation of a new task. Verifier outcome and deterministic dimensions (cost, latency, tool efficiency) are still recorded, and the episode is retained as the reference for thatTaskId— so the second run on that task grades normally.BudgetExhausted— the tenant is over its grading ceiling. Not a quality signal; a spend signal.InsufficientGroup— tournament only. A group closes on quorum or on a timeout, grading whatever arrived, with group size attached to the confidence interval.
- A closed group is immutable, and the next episode starts a new one. A group
closed on timeout at G = 3, then a fourth episode with the same
TaskIdarrives an hour later: re-opening and re-fitting is the wrong answer, because strengths from that group have already been published, aggregated and possibly acted on. A fit that silently changes underneath a decision already made is worse than a small group. - So the group key carries a generation:
(TenantId, TaskId, VerifierOutcome, GroupEpoch). Closure increments the epoch; late arrivals accumulate into the next one. - The cost is honest and should be stated rather than discovered: a low-volume
tenant with a long inter-arrival time gets a run of G = 1 epochs, each
InsufficientGroup. That is a real signal about their volume; the fix is a longer timeout, which is a tenant-level tradeoff between waiting and grading. - A tenant whose loop never engages must see that as a stated reason. Silent no-op is the worst outcome: it looks like a working loop that finds no improvements.
- Every one of these runs reaches terminal
Ungradedand is therefore reducible (T4.4). Without that, they rest inGradingforever and strand storage. - Group timeout default is unsettled — it trades grading latency against group size and depends on tenant arrival rate. Only bites when the tournament is on.
Steps
- Define
UngradedReason { NoReference, BudgetExhausted, InsufficientGroup { group_size } }. NoReferencepath: on a novelTaskId, emitUngraded, record the deterministic dimensions, and register the episode as that task's reference. The registration is the half that is easy to omit.BudgetExhaustedpath: check the tenant ceiling at the group boundary, before any model call. Emit the reason; never a default score.InsufficientGrouppath (tournament only): implement quorum-or-timeout closure, attachgroup_size, and incrementGroupEpochon close.- Route a late arrival into epoch N+1. Epoch N's stored strengths are never recomputed.
- Transition the run to terminal
Ungradedin every case, carrying the reason. - Emit
Ungradedcounts by reason as a metric, separately fromGraded.
Acceptance
- A novel
TaskIdyieldsNoReferenceand the episode is retained as that task's reference, so the second run on the same task producesRelative— assert the second run, since retaining the reference is the half easily omitted. - A tenant over its ceiling yields
BudgetExhausted, never a default score. - Tournament: a group that never fills closes on timeout with size recorded; a late episode lands in epoch N+1 leaving epoch N's fitted strengths byte-identical — assert on the stored strengths, not on the absence of a re-fit call.
- In every case the run reaches terminal
Ungradedand is therefore reducible (T4.4).
Phase gate
P5 closes when PairwiseSequential decides an accept and a reject against a mock
judge, on one resident model with zero swaps.
Verify
Harness: a fresh tenant with no history (for NoReference), a tenant with a
zero grading ceiling (for BudgetExhausted), and a tournament group that never
fills (for InsufficientGroup).
Integration test — tests/it_degradation_reasons.rs:
- Novel
TaskId: assertScore::Ungraded { NoReference }, and that deterministic dimensions (cost, latency) are still recorded. - The second half, which is the one that gets omitted: run a second
episode on the same
TaskId; assert it producesScore::Relative. That only works if run 1's episode was retained as the reference. - Budget: tenant over its grading ceiling →
BudgetExhausted. Assert zero model calls were made — the check must precede the spend, not follow it. Assert no default score anywhere in the output. - Tournament group: a group that never reaches quorum closes on timeout.
Assert
InsufficientGroup { group_size }with the real size recorded. - Epoch immutability: snapshot epoch N's fitted strengths. Deliver a late
episode on the same
TaskId. Assert it lands in epoch N+1 and that epoch N's stored strengths are byte-identical to the snapshot. - Terminal state: for every one of the three reasons, assert the run reaches
terminal
Ungradedand that T4.4'sis_reduciblereturns true. - Assert
Ungradedcounts are emitted by reason as separate metric series.
Command: cargo test -p grading degradation
False pass:
- Step 5 asserting "no re-fit call was made". A refactor that recomputes lazily on read passes that and still changes a published number. Assert on the stored strengths.
- Step 2 omitted: emitting
NoReferenceis trivial; retaining the reference is the half that makes the loop ever engage, and nothing else detects its absence. - Step 6 omitted: a run left in
Gradinglooks correct in the score output and strands storage forever for exactly the tenants this section exists to accommodate. - Step 3 checking only the reason. If the ceiling is enforced after the calls, the reason is right and the money is gone.
Traps
- Emitting a neutral 0.5 for an ungradable episode. It averages into promotion gates and looks like data.
- Re-opening a closed group for a late arrival.
- Leaving the run in
Grading, which strands storage for exactly the low-volume and cost-capped tenants least able to absorb it.
Background (not required to do this task): rust-agentic-sys.md §5.2, §11.6, §14, §15, §18 · rust-agentic-task.md