feat(harness): retained tool outputs — big results live on disk, not in context - #64
Open
pierreprudh wants to merge 2 commits into
Open
feat(harness): retained tool outputs — big results live on disk, not in context#64pierreprudh wants to merge 2 commits into
pierreprudh wants to merge 2 commits into
Conversation
… context Prime-Agent-inspired L2 store: a tool result over 20KB is written to <runs_dir>/<run_id>.values/<call_id>.txt and the provider messages carry a [retained #id] stub with a head/tail preview instead. The transcript event keeps the full result, so the durable record is unchanged and both replay shapes (text-fold and structured) rebuild the same stub deterministically. A new peek_value tool (conversation-history capability) reads numbered line ranges or searches the stored text, with caps that keep any peek below the retention threshold — a peek can never itself be retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…est replay, gated stubs
Review of the retained-values slice surfaced four fixable gaps; all closed:
- peek_value now numbers lines with the same `N: ` gutter read_file uses,
and write_file's tolerant matcher strips *stacked* gutters — so a line
quoted from a peeked slice of a retained read (two gutters deep) still
edits cleanly. This was the one real interop break: editing a >20KB file
went read → stub → peek → paste, and the paste failed to match.
- Replay only stubs a prior result when its value file is actually on disk;
otherwise the full text rides along. A stub can no longer point at a value
that never landed (failed live write) — impossible by construction now,
not just on the live turn.
- Retention is gated on peek_value being in this run's tool list (mode +
disabled tools): no stub may instruct the model to call a tool it
doesn't have. Live and replay share the same gate.
- The text-fold replay learns tool names from the assistant turns, so its
stub reads exactly as the live loop wrote it ("read_file returned …",
not "tool returned …") — both replay shapes now byte-match the live stub.
- value ids that sanitization actually altered get a short FNV-1a suffix,
so two distinct call ids can't collapse onto one file; clean provider ids
pass through untouched.
New coverage: a run_loop e2e test drives the real loop through
read → stub → peek_value → answer and pins the provider view, the value
file, and the untouched transcript; replay tests pin the no-file-full-text
fallback and the stacked-gutter edit match. 609 lib tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
|
Review follow-up (a4191a3) — the four fixable cons from the pros/cons pass are closed:
Plus a run_loop e2e test driving the real loop through read → stub → peek → answer. 609 lib tests pass, Deliberately left open (noted, not blocking): the 20KB threshold isn't in harness settings, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Prime-Agent-inspired (arXiv 2608.23552) retained-values store for the Rust harness:
<runs_dir>/<run_id>.values/<call_id>.txt; the provider messages carry a[retained #id]stub with a head/tail preview instead of the full text.peek_valuetool (conversation-history capability) reads numbered line ranges (≤400/call) or searches the stored text by substring. Peek output is capped at 16KB — below the retention threshold by construction, so a peek can never itself be retained.ToolCallFinishedevent keeps the full result: the durable transcript is unchanged, Mission Control and the UI still show everything, and both replay shapes (text-fold and structured) rebuild the identical stub deterministically from the transcript — a resume can't re-poison the context.Compaction remains the second line of defense; retained results are no longer lost when it runs.
Before / after
Before — every byte of a tool result rides in the provider messages until auto-compaction guts it to a 600-char excerpt; the original is gone and the model must re-run the tool:
flowchart LR T["tool executes<br/>(103KB output)"] --> C["provider messages<br/>full 103KB in context"] C -->|"turns pass"| K["compaction<br/>600-char excerpt"] K -->|"original lost"| R["model re-runs the tool<br/>to see it again"] T --> E["transcript event<br/>full result"]Now — the full text lands on disk once; context carries a small stub; the model reads slices on demand and nothing is ever lost:
flowchart LR T["tool executes<br/>(103KB output)"] --> G{"> 20KB?"} G -->|no| C["provider messages<br/>verbatim, as before"] G -->|yes| D["runs/<run>.values/<call>.txt<br/>full text, atomic write"] G -->|yes| S["provider messages<br/>[retained #id] stub<br/>head + tail preview"] S -.->|"model calls peek_value<br/>(range or query, ≤16KB)"| D T --> E["transcript event<br/>full result — UI, replay,<br/>Mission Control unchanged"]The same stub is rebuilt deterministically on replay/resume (pure
stub_forover the transcript event), so a continuation turn sees exactly what the live turn saw.Verified live
Dogfooded in a real Goal run (deepseek-v4-flash): a 103KB
package-lock.jsonread was retained, the model searched it withpeek_value(query: "node_modules/vite"), drilled into lines 2445–2458, and answered vite 7.3.6 — correct to the integrity hash — without the file ever entering context.Tests
retained.rs(threshold, stub shape, peek range/query/caps, traversal, live-vs-replay stub equality)cargo test --lib: 604 passed ·cargo check+npx tsc --noEmitclean🤖 Generated with Claude Code