# `Planck.Agent.Hooks.Compactor`
[🔗](https://github.com/alexdesousa/planck/blob/v0.2.2/lib/planck/agent/hooks/compactor.ex#L1)

Behaviour and default implementation for context compaction in `Planck.Agent`.

## Behaviour

Use `use Planck.Agent.Hooks.Compactor` to implement a custom compaction strategy
in a sidecar. Two callbacks are required, `compact?/3` and `compact/3`;
`compact_timeout/0` has a default of 600000 ms.

    defmodule MySidecar.Compactors.Builder do
      use Planck.Agent.Hooks.Compactor

      @impl true
      def compact?(state, context, _recent) do
        Planck.AI.Context.estimate_tokens(context) >= state.model.context_window * 0.8
      end

      @impl true
      def compact(_state, _context, recent) do
        summary = Message.new({:custom, :summary}, [{:text, summarise(recent)}])
        kept    = Enum.take(recent, -5)
        {:compact, summary, kept}
      end

      @impl true
      def compact_timeout, do: 60_000
    end

`compact?/3` is checked first; `compact/3` — the potentially slow part, an
LLM call for the built-in strategy — is only ever called when it returns
`true`. Splitting these means the *dispatcher* (`compact/4` below), not
each implementation, can wrap the slow part with a progress announcement
correctly for any compactor, without needing to predict anything — see
"Why a separate compact?/3, and why the dispatcher wraps compact/3" below.

## Dispatch

`Planck.Agent` calls `compact/4` before every LLM turn:

    Hooks.Compactor.compact(state, context, recent, opts)

- `state` — the full agent state (model, compactor, sidecar_node, ...).
- `context` — the `Planck.AI.Context.t()` built for `recent` (system prompt,
  tool schemas, and `recent` itself, already estimated as one whole — see
  `Planck.AI.Context.estimate_tokens/1`). Passed through rather than just
  `recent` alone so a *custom* compactor (typically running on a sidecar
  node, which has no other way to see the agent's system prompt or tool
  list) can make an informed decision too, not only the built-in one.
- `recent` — `state.messages` since the last `{:custom, :summary}`
  checkpoint (or all of them, if there isn't one yet).
- `opts` — `:on_compacting` and `:on_compacted`, both zero-arity functions,
  both optional. Called by *this dispatch function*, around `compact/3` —
  never by a `compact/3` implementation itself, which never receives
  `opts` at all. `Planck.Agent` supplies these so the UI can be told
  compaction is in progress without any compactor needing to know
  anything about `Planck.Agent`'s own PubSub topics or event shapes.

- `state.compactor: nil` — uses `Planck.Agent.Hooks.Compactor.Default`, the
  built-in strategy (see its own moduledoc). Not special-cased beyond this:
  `Default` satisfies the same behaviour a custom module would.
- `state.compactor` set, `state.sidecar_node: nil` — calls `module.compact?/3`,
  then, only if that's `true`, `module.compact/3`, in-process.
- `state.compactor` set, `state.sidecar_node` set — calls the module on the
  remote node via RPC (same two-call shape); falls back to `Default` on
  `:badrpc` from either call.

## Why a separate `compact?/3`, and why the dispatcher wraps `compact/3`

`Planck.Agent` calls this dispatcher on *every* turn, and can't know in
advance whether a given call will actually compact — that decision
belongs to the compactor, and for a custom one, its criteria are opaque to
`Planck.Agent` entirely. Broadcasting "compacting" unconditionally around
every call (clearing it right after) would flash it on every ordinary
turn, not just the rare one that actually compacts. Predicting the
outcome from `Planck.Agent`'s side (e.g. reapplying the built-in ratio)
would only be accurate for the built-in compactor. Splitting the decision
(`compact?/3`, always cheap — no LLM call for the built-in strategy) from
the work (`compact/3`, potentially slow) lets the dispatcher check the
decision first and only announce progress around the part that's
genuinely slow — accurate for any compactor, without `Planck.Agent`
needing to predict anything or any compactor needing to call back into
`Planck.Agent` itself.

# `compact_opts`

```elixir
@type compact_opts() :: [on_compacting: (-&gt; any()), on_compacted: (-&gt; any())]
```

`:on_compacting`/`:on_compacted` — both zero-arity, both optional (neither
given just means nothing tells the UI compaction is in progress, not an
error).

# `compact`

```elixir
@callback compact(
  state :: Planck.Agent.t(),
  context :: Planck.AI.Context.t(),
  recent :: [Planck.Agent.Message.t()]
) :: compact_result()
```

Compact the conversation. Only ever called when `compact?/3` (checked by
the dispatcher, not called here) already returned `true`.

Return `{:compact, summary_msg, kept}` to replace older messages with a
summary, or `:skip` to leave the list unchanged — a compactor is free to
still decide against compacting here even after saying `true` to
`compact?/3` (e.g. nothing old enough left worth summarizing).

# `compact?`

```elixir
@callback compact?(
  state :: Planck.Agent.t(),
  context :: Planck.AI.Context.t(),
  recent :: [Planck.Agent.Message.t()]
) :: boolean()
```

Cheap decision: would `compact/3` actually do anything right now? Must not
itself do anything slow (no LLM call) — see the moduledoc for why.

# `compact_timeout`

```elixir
@callback compact_timeout() :: pos_integer()
```

RPC call timeout in milliseconds when this compactor is invoked remotely.

Defaults to 600000 ms.

# `compact`

```elixir
@spec compact(
  Planck.Agent.t(),
  Planck.AI.Context.t(),
  [Planck.Agent.Message.t()],
  compact_opts()
) ::
  compact_result()
```

Dispatch compaction for the given agent state, its built request context,
and the messages since the last summary — see this module's own moduledoc.

Returns `:skip` or `{:compact, summary_msg, kept}`.

# `default_compact_timeout`

```elixir
@spec default_compact_timeout() :: pos_integer()
```

Default RPC timeout used when a compactor module omits `compact_timeout/0`.

---

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