feat(agentdiet): add the published AgentDiet trajectory-reduction baseline - #66
Open
itay-nakash wants to merge 1 commit into
Open
feat(agentdiet): add the published AgentDiet trajectory-reduction baseline#66itay-nakash wants to merge 1 commit into
itay-nakash wants to merge 1 commit into
Conversation
…eline
Adds `agentdiet`, a reproduction of the method in "Reducing Cost of LLM Agents
with Trajectory Reduction" (Xiao, Gao, Peng, Xiong -- FSE 2026,
arXiv:2509.23586), plus an `agentdiet` preset that runs it at the paper's tuned
hyperparameters. It is offered as a COMPARABLE BASELINE, not a recommendation:
it lets the published approach be A/B'd against extract_llm and summarize on the
same traffic, agent and benchmark.
Its unit is the STEP -- one assistant message plus the tool results answering it
-- and four things follow from that which no existing component here does:
* A fixed AGE picks the target, not size or economics: when the agent has
completed step s, only step s-a is eligible (delay_steps, a=2). The most
recent a steps are untouchable, so a bad reduction cannot corrupt what the
agent is working on now.
* The model gets a sliding WINDOW of neighbouring steps as XML
(context_steps, b=1 => steps [s-a-b .. s]). This is what extract_llm
structurally cannot do, and it is what lets the model call content
*redundant* or *expired* rather than merely verbose.
* Two thresholds bound the spend: a step below min_step_tokens (theta=500)
never earns a call, and a reduction is applied only if it clears
min_saved_tokens or max_keep_ratio.
* A reduction is made once, then frozen and replayed byte-identically, so the
prefix stays stable and reductions accumulate across the session.
Three deviations, all forced by where context-guru sits, documented in
docs/components/agentdiet.md: the reduction is written in place into the step's
tool results (only summarize may change the message count) rather than replacing
the step with one assistant message; tool-call arguments are not reduced because
bifrost's schema does not model Anthropic tool_use blocks; and the prompt is
written from the paper's description rather than copied from its artifact.
cache_tail_only defaults to FALSE, unlike every other age-based offloader here.
The target is chosen by age, so with a>=1 it is always inside the cached prefix
and a tail restriction would make the component a silent no-op on caching
backends. The paper accepts one suffix cache-write per reduced step and counts
it; the freeze makes that once per step, not once per turn.
Fail-open is countable: CONTEXT_GURU_AGENTDIET_TIMEOUT bounds one reflection
call and /stats reports agentdiet_timeouts / agentdiet_errors /
agentdiet_call_timeout_ms, so an arm that quietly stops reducing cannot be
mistaken for one that got faster.
The reply parser declines one case rather than guessing it. A `<result>` block
whose id is missing or unparseable is read as id 0 only when the step has a
single result; on a step with PARALLEL tool calls it is dropped. Defaulting it
to 0 there would splice one tool's compressed output into another tool's
message, which is a MISATTRIBUTION rather than a lossy reduction: the
never-worse token check cannot see it (the text is smaller, merely wrong), the
agent reads an answer that never belonged to the call above it, and the decision
is then frozen and replayed for the rest of the session. The prompt does ask the
model to reproduce every id unchanged; the parser does not depend on it having
obeyed, because it has no stop sequence to enforce the shape. Pinned by four
parser cases and one end-to-end test, all four verified to fail when the guard
is removed.
delay_steps: 0 is still accepted, for an ablation, but it is now documented as
giving up the protection above -- it targets the step the agent has just
completed, so the "most recent a steps are untouchable" property holds only for
a >= 1.
Also adds two config tests that were missing: every preset must BUILD (a
name-list is only a promise until the registry resolves it), and a rich preset's
pipeline must match its `presets` entry (or ?preset= means something different
over HTTP than in a file). The first found that `coding` cannot build without
-tags cg_skeleton; it skips that case explicitly rather than papering over it.
Verified: unit tests cover the age window, the sliding window, both thresholds,
reversibility, freeze/replay, the parallel-call parser guard, and the
fault-injection paths (timeout, model error, no model, unparseable reply). End
to end through the proxy on a claude-code-shaped Anthropic body,
PRESET=agentdiet against a stub reflection model: exactly one call per turn,
only the step at age 2 reduced, window = [s-1 .. s+2], reversible marker
present, and the reduced step replayed byte-identically on the next turn with no
second call for it.
Also guards the agent-retry case: two consecutive requests of the same depth
target the same step, and after a replay the message holds the reduced text, so a
frozen lookup keyed on its content hash misses and (under marker_mode: off) there
is no placeholder for skipReduce to catch. The theta gate usually masks this
because a reduced step falls below it -- but a big output whose reduction stays
above theta would be reduced twice, the second time from already-reduced bytes.
Phase 2 now skips indices phase 1 replayed; the test fixture is sized to keep the
reduction above theta so it actually exercises the path (2 model calls without the
guard, 1 with it).
Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Itay-Nakash <itay.nakash@ibm.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.
Why
Every efficiency number in this repo is measured against
off. Nothing here has beencompared against a published method on the same traffic, so there is no way to answer
"is context-guru's approach better than what the literature does, or just better than
nothing?"
AgentDiet (Xiao, Gao, Peng, Xiong -- FSE 2026, arXiv:2509.23586) is the closest published
analogue: LLM-driven trajectory reduction on agentic coding traffic, reporting -39.9% to
-59.7% input tokens and -21.1% to -35.9% total cost at unchanged task success on SWE-bench
Verified and Multi-SWE-bench Flash. This PR adds it as a component and a preset so it can
be run as an arm beside
extract_llmandsummarize, on one agent, one model and onebenchmark.
It is offered as a BASELINE, not a recommendation, and the preset list says so.
What
agentdiet(Offload, LLM) plus anagentdietpreset at the paper's tuned hyperparameters.Its unit is the STEP -- one assistant message plus the tool results answering it -- and
four consequences of that are things no existing component here does:
step s, only step s-a is eligible (
delay_steps, a=2). The most recent a steps areuntouchable, which is the paper's protection against a malfunctioning reflection model:
a bad reduction cannot land on what the agent is working on right now.
context_steps, b=1,so steps [s-a-b .. s]). This is the part
extract_llmstructurally cannot do, and it iswhat lets the model call content redundant (already stated nearby) or expired
(mattered only to a finished sub-goal) rather than merely verbose.
expiredis a signalnothing else here computes.
min_step_tokens(theta=500) never earns acall, and a reduction that comes back is applied only if it clears
min_saved_tokensormax_keep_ratio.prefix stays stable and reductions accumulate over the session -- what the paper gets
for free by editing the agent's own trajectory in place.
Defaults are the paper's a=2, b=1, theta=500.
min_saved_tokens(400) andmax_keep_ratio(0.8) come from the authors' artifact, whose apply-gate issaved >= 400 || keep < 0.8; Algorithm 1 in the paper states it more simply asl_orig - l_reduced > theta. The artifact's form is the default because it is whatproduced the published numbers. Set
min_saved_tokens: 500andmax_keep_ratio: 0forthe paper's stated gate.
Three deviations, all forced by where context-guru sits, documented in
docs/components/agentdiet.md:reflection module replaces the whole step with one assistant message; here only
summarizemay change the message count. Tool results are 63% of trajectory tokens bythe paper's own accounting (30.4K of 48.4K), so this keeps most of the mass.
tool_useblocks, so their name and input are invisible to every component here and theassistant message is not even Rewritable. That forgoes the paper's
str_replace_editorredundancy case, ~25% of trajectory tokens, and is the main structural reason to expect a
smaller reduction here than 39.9%-59.7%.
from the artifact.
components.Modelis one prompt in / text out, so the paper's system</step>stop sequence is folded into one prompt and thereply is parsed defensively instead.
cache_tail_onlydefaults to FALSE, unlike every other age-based offloader here, and thatis the faithful setting rather than an oversight. The target is chosen by AGE, so with
a >= 1 it is ALWAYS inside the provider's cached prefix; a tail restriction would make the
component a silent no-op on every caching backend, which is the trap #28 left
extract_llmin. The paper accepts one suffix cache-write per reduced step and prices it, and because
the decision is then frozen, that write happens once per step rather than once per turn.
Fail-open is countable.
CONTEXT_GURU_AGENTDIET_TIMEOUT(default 90s) bounds onereflection call, and
/statsreportsagentdiet_timeouts,agentdiet_errorsandagentdiet_call_timeout_ms. Its own budget rather than reusingCONTEXT_GURU_LLM_TIMEOUTbecause the prompts differ by roughly the window multiple:
extract_llmsends one tooloutput, this sends b+1+a serialized steps. Separate counters for the same reason as
summarize's -- this runs in its own arm, so folded into
llm_*an agentdiet arm whoseevery call expired would report
llm_timeouts0 and read as having nothing to reduce.One case in the reply parser is DECLINED rather than guessed. A
<result>block whoseidis missing or unparseable is read as id 0 only when the step has a single result; on a step
with parallel tool calls it is dropped. Defaulting it to 0 there would splice one tool's
compressed output into ANOTHER tool's message, and that is a misattribution rather than a
lossy reduction: the never-worse token check cannot catch it because the text is smaller,
merely wrong; the agent then reads an answer that never belonged to the call above it; and
the decision is frozen and replayed for the rest of the session. The prompt does ask the
model to reproduce every id unchanged, but there is no stop sequence to enforce the shape,
so the parser must not depend on it having obeyed.
Also adds two config tests that were missing: every preset must BUILD (a name-list is only
a promise until the registry resolves it), and a rich preset's pipeline must match its
presetsentry, or?preset=means something different over HTTP than in a file. Thefirst found that
codingcannot build without-tags cg_skeleton; it skips that caseexplicitly rather than papering over it.
Measured, and it does not win here
Run as a fourth arm against
offand two of our own pipelines. SWE-bench Verified, 50tasks, SEED=0 so all arms see the same task set, claude-code,
Qwen/Qwen3.6-27Bonself-hosted vLLM (TP=2, 128k, concurrency 4), FOUR replicate trials -- 798 trials total, 0
harbor exceptions. All figures below are MATCHED: only tasks that scored and were
exception-free in all four arms of that trial are compared, because averaging each arm over
its own survivors is survivor bias and has inverted a sign in this workspace before.
Context per turn, two independent measures.
final_ctxis the last prompt of thetrajectory;
tok/stepis cumulative prompt tokens divided by step count, which is thefigure that does not simply grow with step count:
Both measures agree: the published method reduces essentially NOTHING on this traffic
(+0.9%, SD 4.0 -- the effect is smaller than the run-to-run spread), while our own
deterministic reducers cut 23-24% on the same tasks with an SD of about 3.
And it is not free. Agent exec time per task, matched:
That is one reflection call on the request's hot path every turn:
cg_added_ms_avg2,862to 4,260 ms across the four trials. Reward is parity, not a loss -- 0.646 vs 0.682, i.e.
under two tasks in fifty, and two untreated baselines here flip 7 of 50 task outcomes, so
14% of tasks is the noise floor and no reward delta in this run clears it.
The cause is the CADENCE, not the implementation, and
/statssays so rather than leavingit to inference. 624 reflection calls against 2,442 proxy requests is one call per four
turns -- theta=500 plus the benefit gate decline the other three -- i.e. ~12.5 calls per
trial against a mean 50.5 steps, and not every call is applied. One step reduced per turn
cannot offset a trajectory that grows one step per turn, which is also why
maskwins onthis traffic: it reduces everything older than
keep_recent, not one step. The freezeitself is sound where it was exercised --
agentdiet_timeouts0,reverted0,discarded_changes0 across all four trials,frozen_droppedandfrozen_repaired0, andfrozen_hits3,511 and 1,630 in trials 1 and 2, i.e. reductions do freeze and do replaybyte-identically. See the third bullet below for trials 3 and 4.
Two things this is NOT. It is not a refutation of the paper: the reflection model here is a
27B open model on a shared GPU, where the paper used GPT-5 mini against Claude 4 Sonnet
(~12x cheaper than the agent, and much stronger than ours), and the
tool_usedeviationabove puts ~25% of the trajectory permanently out of reach in this position. It is also not
a result about
min_step_tokens: lowering theta below the paper's 500 would raise thereduction count, but it would no longer be a reproduction, which is the whole point of
having it in tree.
So the shipped shape is the honest one --
agentdietexists as a baseline arm and appearsin no recommended preset, and its preset carries no other offloader so that whatever it
does is attributable to it.
Testing
Component tests cover the age window, the depth guard, the sliding window, both thresholds,
reversibility, freeze and replay, the parallel-call parser guard, and every fail-open path
(deadline, model error, no model, unparseable reply). Two mutation checks, because a test
that cannot fail proves nothing:
including the end-to-end one, with "result 0 was rewritten from an unlabelled block".
to reach the double-reduce path: 2 model calls without the phase-2 guard, 1 with it. The
theta gate would otherwise mask it, since a reduced step normally falls below theta.
End to end through the proxy on a claude-code-shaped Anthropic body with
PRESET=agentdietand a stub reflection model: exactly one call per turn, only the step atage 2 reduced, window [s-1 .. s+2], reversible marker present, and the reduced step
replayed byte-identically on the next turn with no second call for it.
What to scrutinise
</result>truncates that same id's body early. It cannot move text to a different id(scanning always takes the leftmost
<result), so the failure mode is over-truncation ofthe correct message, which is reversible through the marker like any other reduction.
Left as is rather than escaping, because escaping would change what the model sees.
savings_pctfor this arm reads 11.52% while final context is unchanged. That is theinflated-denominator effect:
adjusted_savedcounts tokens removed across REPEATEDsends, not a cut in what the model reads per turn. The tables above deliberately use
final_ctxand tok/step instead. Worth knowing before anyone quotes the proxy's ownpercentage for this component.
delay_steps: 0is accepted, for an ablation, and now documented as giving up theprotection the component is otherwise built around -- it targets the step the agent has
just completed, so "the most recent a steps are untouchable" holds only for a >= 1. It is
not rejected outright because the ablation is legitimate.
in trials 3 and 4 the freeze store stopped holding decisions for EVERY treated arm, not
just this one.
codesafe-lo-swewent from 33,095frozen_hits/ 8,392frozen_droppedin trial 1 to 68 / 70,391 in trial 3, on an identical config, and
agentdiet-lofrozen_hitswent 3,511 -> 0. Those trials ran longer trajectories (3,396 vs 2,924 proxyrequests on that arm), which points at the TTL / pin-cap budget in
store.Memoryratherthan at any one component --
frozen_flipsreached 70,380, and each flip is a suffixcache-write at ~11.5x the read price. Worth a look independently of this baseline, since
it silently degrades whichever offloader is deployed.