Deterministic pressure-triggered trimming strategy.
Keeps:
- The first user message (when
keep_initial_user: true) — the first message with role:userin the input list, defined as the head, not a re-derivation from turns. - The last
keep_recent_turns × 2messages.
Drops everything in between when triggered. Pure function — no LLM calls, no state mutations.
Triggers
trigger[:turns]— fires whenctx.turn > N.trigger[:tokens]— fires when estimated total message tokens ≥N.
Both may be set; either firing triggers compaction (OR, not AND). When not triggered, returns the input unchanged.
Edge cases
- Fewer messages than
keep_recent_turns × 2 + 1→ input unchanged,triggered: false. - First message not
:user→ skip initial-user retention;kept_initial_user?: false. - Recent slice begins with
:assistant→ drop one more from the front so it starts with:user. - Single retained message exceeds token budget → keep it whole, set
over_budget?: true.
Token estimation is a pressure heuristic, not adapter-accurate.
Summary
Functions
Run the trim strategy.
Types
@type message() :: %{role: :user | :assistant, content: String.t()}
@type stats() :: %{ enabled: boolean(), triggered: boolean(), strategy: String.t(), reason: :turn_pressure | :token_pressure | nil, messages_before: non_neg_integer(), messages_after: non_neg_integer(), estimated_tokens_before: non_neg_integer(), estimated_tokens_after: non_neg_integer(), kept_initial_user?: boolean(), kept_recent_turns: non_neg_integer(), over_budget?: boolean() }
Functions
@spec run([message()], PtcRunner.SubAgent.Compaction.Context.t(), keyword()) :: {[message()], stats()}
Run the trim strategy.
Always returns {messages, stats}. Use stats.triggered (boolean) to
distinguish triggered from not-triggered runs — the stats map always has
the same shape, so callers can rely on every field being present.