feat(context): let the host word a Tier 1 card's recovery footer - #31
Merged
Merged
Conversation
A Tier 1 mini card REPLACES the whole tool message. The loop's own truncation footer (``_spill_footer``) appends to a surviving body and can therefore afford to be the only place that explains recovery — a card cannot, because after it runs there is nothing beside the handle at all. On a Tier1-only turn (Tier 2 fires only when Tier 1 did not free enough) the model sees a bare ``[Full text] <handle>`` and is never told the handle is recoverable, let alone how. The default cannot fix that itself. This module knows neither what a host calls its recovery tool nor whether that tool is bound for the agent whose history it is compacting, and a footer naming a tool the agent cannot call is worse than no footer — precisely why ``_spill_footer`` gates its prose on ``recover_result`` being in the tool map. So the handle-only rendering stays the default, and the decision moves to the party that has the tool map: ``recovery_footer``. ``default_recovery_footer`` is the existing wording, so no consumer changes behaviour by upgrading; both its docstring and the CHANGELOG carry the one constraint a host has to respect. Write the prose as prose. The callable form ``recover_result(spill_id="...")`` reads to the model as source code and it answers in kind: on a live run a model reproduced such a footer inside a ```bash block instead of emitting a tool call, and ``LeakedToolCallRetryObserver`` fired twice. That failure is why the wording was flattened to ``[Full text]`` in the first place — the fix is prose naming the tool, not a return to the callable form. Tests pin the three things a host renderer must not be able to break: the default names no tool, a host footer replaces the last line and leaves the card's arg/URL lines (and so its token budget) untouched, and the footer is never invoked when nothing spilled. A fourth pins idempotency against a footer LONGER than the default, since that is the case where a second pass re-carding the message would compound. MINOR per docs/versioning.md: new capability, additive keyword argument. Generated with [Claude Code](https://claude.ai/code) via [Very Happy](https://github.com/Mereithhh/very-happy) Co-Authored-By: Claude <noreply@anthropic.com>
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
Adds a
recovery_footer: Callable[[str], str]keyword toKeepLastNToolResultsCompactor, and forwards the same callable throughTieredCompactorto the Tier 1 compactor it builds internally. The default,default_recovery_footer, renders[Full text] {spill_path}— byte-for-byte what the compactor renders today, so no caller sees a change.0.7.0 → 0.8.0perdocs/versioning.md: new capability, additive keyword argument with a default (non-breaking).Why
A Tier 1 card's last line is a handle and nothing else, deliberately: this module knows neither what a host calls its recovery tool nor whether that tool is bound for the agent whose history it is compacting. A footer naming a tool the agent cannot call is worse than no footer — which is exactly why
_spill_footergates its own prose onrecover_resultbeing in the tool map.But a card has no such surviving neighbour.
_spill_footerappends to a message that still carries other text; a card replaces the whole tool message. So on a turn where only Tier 1 ran — and Tier 2 fires only when Tier 1 did not free enough — the model is shown a bare id with nothing beside it saying the body is still fetchable. Functionally nothing is lost (spill_refsis set on the same message and the recovery index reads it first); what is lost is the model knowing.That makes it a host injection point rather than an upstream wording bug: a host that does bind a reader, and knows when, can say so. This PR opens that point and changes nothing else.
The prose constraint, documented in the code
default_recovery_footer's docstring carries the measured reason a host must write prose, not a call:recover_result(spill_id="...")renders to the model as source code and it answers in kind — on a live run a model reproduced exactly such a footer inside a ```bash block instead of emitting a tool call, andLeakedToolCallRetryObserverfired twice. Without that written down, the first host to use this hook re-derives the anti-pattern.Why
TieredCompactorforwards rather than defaultsTier 1 is a winning candidate in its own right (
best_label == "tier1"), so its card reaches the model from the tiered path exactly as from the standalone one. A workflow whose primary path is tiered and whose fallback is standalone would otherwise render the default on every production turn and the host's wording only in the fallback — the harder of the two failures to notice.Tests (6 new)
Standalone (
tests/test_keep_last_n_compactor.py):hosted[:-1] == default[:-1], so a host cannot silently change the card's token budgetTiered (
tests/test_tiered_compact.py):Idempotency is unaffected by wording throughout: the already-carded check keys on
OMITTED_TOOL_RESULT_PLACEHOLDER, not on the footer.Verification
uv run pytest -q→ 1474 passed, 1 skippeduv run ruff check .→ All checks passedscripts/check_version_bump.py --base main→ "Published code changed and version increased 0.7.0 -> 0.8.0"scripts/check_unconsumed_fields.py→ clean[0.8.0]sectionConsumer
ApodexHarness has the corresponding host-side adapter ready and held locally pending this release. On a
>= 0.8.0pin itscore/runtime/loop/compact.pycollapses from acompact()-overriding subclass to a singlerecovery_footer=argument, and — the part it cannot do today — the tiered path becomes reachable at all, since it aliasestiered_compactwholesale and so cannot subclass into it.🤖 Generated with Claude Code