139 lines
6.7 KiB
Markdown
139 lines
6.7 KiB
Markdown
# 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:
|
||
|
|
|
||
|
|
1. **`NoReference`** — a novel `TaskId`. 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 that `TaskId`** — so the
|
||
|
|
second run on that task grades normally.
|
||
|
|
2. **`BudgetExhausted`** — the tenant is over its grading ceiling. Not a quality
|
||
|
|
signal; a spend signal.
|
||
|
|
3. **`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 `TaskId`
|
||
|
|
arrives 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 `Ungraded` and is therefore reducible
|
||
|
|
(T4.4). Without that, they rest in `Grading` forever 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
|
||
|
|
|
||
|
|
1. Define `UngradedReason { NoReference, BudgetExhausted, InsufficientGroup { group_size } }`.
|
||
|
|
2. `NoReference` path: on a novel `TaskId`, emit `Ungraded`, record the
|
||
|
|
deterministic dimensions, and **register the episode as that task's
|
||
|
|
reference**. The registration is the half that is easy to omit.
|
||
|
|
3. `BudgetExhausted` path: check the tenant ceiling at the group boundary, before
|
||
|
|
any model call. Emit the reason; never a default score.
|
||
|
|
4. `InsufficientGroup` path (tournament only): implement quorum-or-timeout
|
||
|
|
closure, attach `group_size`, and **increment `GroupEpoch` on close**.
|
||
|
|
5. Route a late arrival into epoch N+1. Epoch N's stored strengths are never
|
||
|
|
recomputed.
|
||
|
|
6. Transition the run to terminal `Ungraded` in every case, carrying the reason.
|
||
|
|
7. Emit `Ungraded` counts **by reason** as a metric, separately from `Graded`.
|
||
|
|
|
||
|
|
## Acceptance
|
||
|
|
|
||
|
|
- A novel `TaskId` yields `NoReference` **and** the episode is retained as that
|
||
|
|
task's reference, so the **second** run on the same task produces `Relative` —
|
||
|
|
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 `Ungraded` and 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`:
|
||
|
|
1. **Novel `TaskId`:** assert `Score::Ungraded { NoReference }`, and that
|
||
|
|
deterministic dimensions (cost, latency) are still recorded.
|
||
|
|
2. **The second half, which is the one that gets omitted:** run a **second**
|
||
|
|
episode on the same `TaskId`; assert it produces `Score::Relative`. That only
|
||
|
|
works if run 1's episode was retained as the reference.
|
||
|
|
3. **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.
|
||
|
|
4. **Tournament group:** a group that never reaches quorum closes on timeout.
|
||
|
|
Assert `InsufficientGroup { group_size }` with the real size recorded.
|
||
|
|
5. **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.
|
||
|
|
6. **Terminal state:** for every one of the three reasons, assert the run reaches
|
||
|
|
terminal `Ungraded` and that T4.4's `is_reducible` returns true.
|
||
|
|
7. Assert `Ungraded` counts 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 `NoReference` is 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 `Grading` looks 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](../../../rust-agentic-sys.md) §5.2, §11.6, §14, §15, §18 ·
|
||
|
|
[rust-agentic-task.md](../../../rust-agentic-task.md)
|