3.8 KiB
3.8 KiB
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)
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
- Define
EpisodeView— run metadata, branch list with fork points, attempts per branch — andAttemptViewexactly as above. - Implement the query against materialized state, defaulting to the live branch
with an explicit
all_branchesoption for grading. - 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. - Include every attempt regardless of outcome. Sort by
(BranchId, AttemptNo)so ordering is stable. - 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:
- Build the run: 3 steps, one step failing twice, then a rewind at step 2 producing a second branch.
- Query the episode from materialized state →
view_a. - Drop the state tables; query again, served by cold re-fold →
view_b. - Assert
serialize(view_a) == serialize(view_b). - Assert
view_acontains all attempts including the two failures — count them explicitly, do not just check non-empty. - Assert the default query returns only the live branch, and that the explicit all-branches mode returns both.
- 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
PartialEqderived 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
BlobRefinto 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 §8.5, §10.1 · rust-agentic-task.md