86 lines
3.3 KiB
Markdown
86 lines
3.3 KiB
Markdown
# T4.3 — Lazy blob access
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Phase | P4 — Verification |
|
||
| Size | S — under 1 day |
|
||
| Status | Not started |
|
||
| Flags | — |
|
||
| Spec | inlined below |
|
||
| Blocks | — |
|
||
|
||
## Goal
|
||
|
||
A verifier that needs no text performs no blob reads.
|
||
|
||
## Facts (inlined — no spec read needed)
|
||
|
||
- Context partitions hold **identifiers** — packed, available-but-not-packed,
|
||
dropped — and are small enough to inline. Prompts and outputs are large and go
|
||
**by reference**.
|
||
- Laziness matters concretely: a verifier that shells out and checks an exit code
|
||
needs none of this. Making every verifier carry prompt text penalizes the
|
||
common case and blows broker payload limits.
|
||
- `VerifierCtx.blobs: Arc<dyn BlobStore>` is the mechanism — the verifier calls
|
||
`get` if and only if it wants a body.
|
||
- `get` may return `None` after reduction (T8.4); that is a normal answer, and a
|
||
verifier must not treat it as an error.
|
||
|
||
## Steps
|
||
|
||
1. Confirm `EpisodeView` carries `BlobRef` only. No `prompt_text` field, no
|
||
eagerly-resolved cache.
|
||
2. Pass the store handle through `VerifierCtx.blobs`.
|
||
3. Instrument the store with a call counter behind a test-only wrapper —
|
||
counting is what turns this from an intention into an assertion.
|
||
4. Write the exit-code verifier used as the acceptance case: it reads the attempt
|
||
state and nothing else.
|
||
5. Write a second, text-reading verifier and assert its call count is exactly the
|
||
number of refs it asked for — no over-fetch, no prefetch-all.
|
||
|
||
## Acceptance
|
||
|
||
- An exit-code verifier records **zero** `BlobStore::get` calls.
|
||
|
||
## Verify
|
||
|
||
**Harness:** a counting `BlobStore` decorator wrapping the real store, injected
|
||
through `VerifierCtx.blobs`.
|
||
|
||
**Integration test** — `tests/it_lazy_blobs.rs`:
|
||
1. Run a real run with an exit-code verifier that reads only attempt state.
|
||
2. Assert the counter is **exactly 0**.
|
||
3. Run a text-reading verifier that resolves 2 refs. Assert the counter is
|
||
**exactly 2** — not more (no prefetch) and not fewer (no silent cache hit
|
||
masking a missing read).
|
||
4. Assert `EpisodeView` has no field holding blob **bytes** — a compile-level
|
||
check if the type is closed, otherwise a serialization size assertion on a run
|
||
with a 1 MB prompt: the view must stay small.
|
||
5. Reduction interaction: delete a blob body, then run the text verifier; assert
|
||
`get` returns `Ok(None)` and the verifier handles it without erroring.
|
||
6. Assert the counter is per-run, not global, so parallel tests do not mask each
|
||
other.
|
||
|
||
**Command:** `cargo test -p verify lazy_blobs`
|
||
|
||
**False pass:**
|
||
- Counting at the store level while a caching layer sits above it. The count then
|
||
measures cache misses, not verifier behaviour. Wrap the handle the verifier
|
||
actually receives.
|
||
- Step 4 omitted: a `prompt_text` convenience field makes every verifier pay, and
|
||
steps 1–3 still pass if the resolution happens at view-build time rather than
|
||
through `cx.blobs`. The size assertion catches that.
|
||
|
||
## Traps
|
||
|
||
- A "convenience" `EpisodeView::prompt_text()` that resolves on access. Every
|
||
verifier then pays, invisibly.
|
||
- Prefetching all refs when the view is built, which is the same regression one
|
||
layer earlier.
|
||
|
||
---
|
||
|
||
Background (not required to do this task):
|
||
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §10.1 ·
|
||
[rust-agentic-task.md](../../../rust-agentic-task.md)
|