Skip to main content
TokenSavingMiddleware is an AgentMiddleware subclass that does three independent things, each toggleable:
  1. Tool-output compression — head+tail truncates ToolMessage bodies once they exceed compress_threshold_chars, leaving the most recent N tool messages untouched.
  2. Early-exit nudge — once the agent has run at least early_exit_min_call_index model calls, evaluates trajectory signals and injects a HumanMessage telling the agent to submit when it appears stuck.
  3. Perplexity compression (opt-in) — applies word-level keep/drop classification to stale messages.
Failures inside the hook are caught and logged; the middleware never breaks the agent loop.

Standalone setup

Stack with ReasonBlocksMiddleware

When stacking with ReasonBlocksMiddleware, place TokenSavingMiddleware last. It runs after steering injections are queued, so any injected content goes out compressed.
When you assemble the stack via ReasonBlocksConfig + build_middleware, set enable_token_saving=True and the ordering is handled for you. See ReasonBlocksConfig.

Tool-output compression

Old ToolMessage bodies in the message history are head+tail truncated once they exceed compress_threshold_chars. The middleware replaces same-id messages via LangGraph’s add_messages reducer, so the history actually shrinks rather than growing. The most recent keep_recent_tool_messages tool messages are exempt — the agent always has full visibility into its current reasoning step.
compress_tool_output is also exposed as a standalone helper:

Early-exit nudge

Once the call index reaches early_exit_min_call_index (default 40), the middleware calls signals_fn(steps) on each before_model. If the returned signals indicate a loop, it injects a single HumanMessage with early_exit_text. The fire condition reads three keys from the dict your signals_fn returns (any key it omits is treated as 0.0):
There is no built-in signals function — you supply one. It takes the trajectory steps and returns those loop-likelihood signals in [0, 1]. Compute them however you like; the server-side monitor scores you already collect via telemetry are one source.
signals_fn defaults to None, which disables the early-exit check even when enable_early_exit=True. You must pass a signals_fn for early-exit to do anything.
The default nudge text:
You appear to be stuck in a loop. Stop investigating and submit your current best answer now using whatever submission tool your task expects. Do not start another investigation.
Override it with early_exit_text="...".

Inspect TokenSavingStats

TokenSavingMiddleware.stats is a TokenSavingStats dataclass with running counters.

Perplexity compression (opt-in)

For long trajectories where head+tail isn’t enough, the middleware can apply LLMLingua-2-style word-level keep/drop classification to stale AIMessage and ToolMessage content. Two staleness tiers, each with its own keep ratio. Decisions are cached per (message_id, target_keep_ratio) so each message is classified only once.
Provide a WordClassifier callable. The shipped factory uses Anthropic Haiku as the classifier:
With enable_perplexity_compression=True but perplexity_classifier=None, perplexity compression is silently skipped.
Perplexity compression calls an LLM classifier per stale-message window. Latency and cost scale with the number of stale windows; the per-(id, ratio) cache amortizes the cost across repeat calls but doesn’t eliminate it. Keep perplexity_recent_cutoff >= 3 so the agent never sees its current reasoning compressed.

Use ReasonBlocksConfig for full control

If you assemble the stack via ReasonBlocksConfig + build_middleware, every field above maps to a ts_* config field. See ReasonBlocksConfig.