# `PtcRunner.SubAgent.Compaction.Trim`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/sub_agent/compaction/trim.ex#L1)

Deterministic pressure-triggered trimming strategy.

Keeps:

1. The first user message (when `keep_initial_user: true`) — the *first*
   message with role `:user` in the input list, defined as the head, not a
   re-derivation from turns.
2. The last `keep_recent_turns × 2` messages.

Drops everything in between when triggered. Pure function — no LLM calls,
no state mutations.

## Triggers

- `trigger[:turns]` — fires when `ctx.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.

# `message`

```elixir
@type message() :: %{role: :user | :assistant, content: String.t()}
```

# `stats`

```elixir
@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()
}
```

# `run`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
