Files
poimen/tasks/T1.7-episode-query-surface.md

104 lines
3.8 KiB
Markdown

# T1.7 — Episode query surface
| Field | Value |
|---|---|
| Phase | P1 — Walking skeleton |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
## Goal
Read an episode — run, branches, attempts, refs — with identical results whether
served from materialized state or a cold re-fold.
## Facts (inlined — no spec read needed)
```rust
pub struct AttemptView {
pub step: StepId,
pub attempt: AttemptNo,
pub state: AttemptState,
pub workflow_version: WorkflowVersion,
pub context: Option<ContextPartition>, // identifiers, small, inline
pub prompt: Option<BlobRef>, // large, by reference
pub output: Option<BlobRef>,
pub tools: Option<ToolInfo>,
pub usage: Option<Usage>,
}
```
- **Include failed attempts.** "Retried three times because context was missing
X" is the learning signal; shipping only the winning attempt discards it.
- Queries default to the **live branch**; grading may read all branches. Only the
live branch is exported.
- Materialized state is a cache of the log. If the query answers differently from
a cold re-fold, the state has hidden inputs — that is T0.8's property applied
to the read path.
- Blob bodies are **not** included. The view carries refs; the caller fetches
what it needs (T4.3).
## Steps
1. Define `EpisodeView` — run metadata, branch list with fork points, attempts
per branch — and `AttemptView` exactly as above.
2. Implement the query against materialized state, defaulting to the live branch
with an explicit `all_branches` option for grading.
3. Implement the same query against a cold re-fold (T0.8's `rebuild`), used as
the fallback when state is absent and as the test oracle.
4. Include every attempt regardless of outcome. Sort by `(BranchId, AttemptNo)`
so ordering is stable.
5. Test: build a run with a retry and a rewind; serialize the view from state and
from a cold re-fold; assert equal.
## Acceptance
- Query returns the same structure whether served from materialized state or a
cold re-fold.
- Failed attempts are present in the returned view.
## Phase gate
P1 closes when a full run is recorded and re-derives byte-identically.
## Verify
**Harness:** one recorded run containing a retry **and** a rewind — the two cases
where the two read paths can diverge.
**Integration test**`tests/it_episode_query_equivalence.rs`:
1. Build the run: 3 steps, one step failing twice, then a rewind at step 2
producing a second branch.
2. Query the episode from **materialized state**`view_a`.
3. Drop the state tables; query again, served by cold re-fold → `view_b`.
4. Assert `serialize(view_a) == serialize(view_b)`.
5. Assert `view_a` contains **all** attempts including the two failures — count
them explicitly, do not just check non-empty.
6. Assert the default query returns only the **live** branch, and that the
explicit all-branches mode returns both.
7. Instrument `BlobStore::get`; assert the query itself makes **zero** calls.
**Command:** `cargo test -p query episode_equivalence`
**False pass:**
- Testing on a linear run with no retry and no rewind. Both paths trivially
agree, and the divergence lives exactly in the cases omitted.
- Step 5 as `assert!(!attempts.is_empty())`. A view returning only the winner
passes that.
- Step 4 with a `PartialEq` derived over unordered collections.
## Traps
- Filtering to the successful attempt "because that is what the UI shows". The
learning loop is the other consumer and it needs the failures.
- Eagerly resolving `BlobRef` into text inside the view. That is T4.3's
regression, introduced one layer earlier.
---
Background (not required to do this task):
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §8.5, §10.1 ·
[rust-agentic-task.md](../../../rust-agentic-task.md)