Skip to content

v0.15.0

Latest

Choose a tag to compare

@yuanhao yuanhao released this 05 Aug 16:57
2e16659

Compaction used to rewrite conversation history in place. A prefix-cache hit requires a byte-identical prefix, so every rewrite of already-sent history costs full price for every token from that point on — automatically on DeepSeek, via cache_control on Anthropic. This release treats that as a design constraint rather than an afterthought.

Measured against a real 0.14.2 checkout, on a synthetic workload whose tool mix (bash 41%, edit/write 36%, read 19%, search 4%) is sampled from 808 archived runs of a production agent built on yoagent, and whose file-size distribution comes from that agent's repository:

session hit rate history rewrites DeepSeek Anthropic
300 turns 93.83% → 95.69% 34 → 8 −9.2% −15.2%
1200 turns 94.24% → 95.39% 169 → 35 −16.9% −20.4%
2400 turns 94.77% → 95.27% 415 → 70 −21.3% −22.8%

The rewrite count is the sharper signal. 0.14.2 kept its hit rate up by compacting constantly down to a small context; a small context caches well, it just costs more to keep rebuilding.

What changed

Compaction stopped churning. Tool-output truncation is now idempotent — the truncation marker is charged against the line budget, so re-truncating returns the text byte for byte. Previously the marker pushed the result over the limit and the next pass re-cut it, restating the count (950 lines truncated became 3 lines truncated) and invalidating the prefix a second time. Markers carry no drifting counts, and generated summaries inherit the timestamp of what they replace, so the same history always compacts to the same bytes.

A compaction bug that can produce provider 400s is fixed. Level 2's boundary could land mid-turn, summarizing away an assistant message while its tool results stayed — an orphaned tool_use/tool_result pair that providers reject. Reproduced on 0.14.2 with keep_recent: 7, keep_first: 4. Boundaries now snap to turn starts.

Tool output is bounded where it can be bounded well. Command output takes a head+tail cut fine — first error at the top, summary at the bottom. A file read does not: the middle is the part that was asked for. So read_file now pages itself (DEFAULT_READ_MAX_LINES, 500) with a header stating the true total, and is exempt from truncation via the new per-tool budgets in tool_output_max_lines_overrides. Everything else is capped as it enters the context rather than retroactively.

The compaction target adapts. A fixed ratio cannot know how fast a session is growing, so the room it leaves collapses as history accumulates — the gap between compactions fell from 36 turns to 22 over a long session. compact_headroom_turns targets the interval directly:

target = budget − turns × growth_per_turn

One interpretable knob — how often you are willing to compact — that held the interval at ~35 turns from a 300-turn session through a 2400-turn one, where a fixed ratio decayed to 22, and that self-adjusts to workload.

Upgrading

yoagent = "0.14" will not pick this up automatically. Three defaults change what the model sees, each revertible in one line:

change revert with
tool output capped on append truncate_tool_output_on_append: false
unqualified read returns 500 lines ReadFileTool { max_lines: usize::MAX, ..Default::default() }
compaction target adapts to growth compact_headroom_turns: None

ContextConfig gained four public fields and ReadFileTool gained max_lines, so exhaustive struct literals need ..Default::default(). default(), new(), from_context_window(), and functional-update syntax are unaffected.

Background

The design, the formula, and the reasoning behind each default are written up in Prompt Caching → Cache-Stable Compaction, including why there is no cost-benefit gate on compaction and why these decisions were judged in dollars rather than hit rate — a larger context can raise the hit rate while raising the bill.

Credit to @lloydzhou: bash-agent documents the economics that prompted this work, and reported the measurement gap that opened #99.

Full detail in CHANGELOG.md.