Pressure-triggered context compaction for multi-turn agents.
Compaction reduces the LLM-input message list when turn count or estimated token usage crosses a threshold. Recent turns are preserved verbatim; older turns are trimmed (Phase 1) or summarized (Phase 2 — not yet implemented).
Phase 1 ships one strategy: :trim. Custom strategy modules and
:summarize are deferred to Phase 2.
Configuration
SubAgent.run(prompt, llm: llm, compaction: true)
SubAgent.run(prompt,
llm: llm,
compaction: [
strategy: :trim,
trigger: [turns: 8, tokens: 12_000],
keep_recent_turns: 3,
keep_initial_user: true,
token_counter: nil
]
)Defaults for compaction: true:
[
strategy: :trim,
trigger: [turns: 8],
keep_recent_turns: 3,
keep_initial_user: true,
token_counter: nil
]Library default is false — compaction is opt-in.
Token estimation
Default counter is String.length(content) / 4, matching the existing
metrics heuristic. Override via token_counter: fun/1. This is explicitly
a pressure heuristic and not model-accurate.
Summary
Functions
Build a Compaction.Context for a given turn.
Default token counter — String.length/1 divided by 4, with a floor of 1
for any non-empty content.
Default trim options used when compaction: true.
Run compaction for a list of LLM-input messages.
Normalize compaction configuration.
Types
Functions
@spec build_context(keyword(), keyword()) :: PtcRunner.SubAgent.Compaction.Context.t()
Build a Compaction.Context for a given turn.
Resolves the token counter from opts (or falls back to the default).
@spec default_token_counter(String.t()) :: non_neg_integer()
Default token counter — String.length/1 divided by 4, with a floor of 1
for any non-empty content.
Mirrors PtcRunner.SubAgent.Loop.Metrics.estimate_tokens/1 so token-pressure
detection still fires on histories made of short messages. Pressure heuristic,
not adapter-accurate.
Examples
iex> PtcRunner.SubAgent.Compaction.default_token_counter("hello world")
2
iex> PtcRunner.SubAgent.Compaction.default_token_counter("hi")
1
iex> PtcRunner.SubAgent.Compaction.default_token_counter("")
0
@spec default_trim_opts() :: keyword()
Default trim options used when compaction: true.
@spec maybe_compact( [message()], PtcRunner.SubAgent.Compaction.Context.t(), normalized() ) :: {[message()], stats() | nil}
Run compaction for a list of LLM-input messages.
normalized is the output of normalize/1. Always returns
{messages, stats | nil}:
- When the strategy is
:disabled, returns{messages, nil}— no work, no stats. - Otherwise dispatches to the strategy and returns its
{messages, stats}result. Usestats.triggered(boolean) to distinguish a triggered trim from a not-triggered pass-through; the stats shape is consistent either way.
@spec normalize(nil | boolean() | keyword()) :: normalized()
Normalize compaction configuration.
Accepts:
nilorfalse→{:disabled, []}true→{:trim, default_opts}keyword()withstrategy: :trim(or unspecified) →{:trim, merged_opts}
Raises ArgumentError for invalid input. The error message is the source of
truth for what Phase 1 supports.