Built-in Planck.Agent.Hooks.Compactor implementation.
Used whenever state.compactor is nil — see Planck.Agent.Hooks.Compactor's
own moduledoc for the dispatch rules. Not special-cased by the dispatcher in
any other way: this module satisfies the same behaviour a sidecar-hosted
custom compactor would, and could be named explicitly via AgentSpec.compactor
too.
Strategy
The messages since the last summary checkpoint are split into a recency
floor (always kept verbatim — mirrors the "at least the last message is
always kept" invariant) and an older bucket. The older bucket is summarized
as one plain-text block by a real, ephemeral Planck.Agent — not asked for
structured per-message output. An earlier version of this asked the
delegate for per-message keep/drop verdicts as JSON; that turned out to be
the wrong shape for a compactor that has to work with whatever model is
configured, not a specific one: producing well-formed JSON with correct
message-id references is a much less reliable task for a weak/small model
than writing a plain summary is, and a compactor that only works with
capable models defeats its own "works everywhere" purpose. Per-message
relevance judgment still has a natural home — a fast classifier-style
model whose native output already is one answer per item — just not here.
The delegate agent has no team_id (invisible to list_team, which only
enumerates registered team members), no session_id (nothing is persisted
for it), and no tools — a pure text-in/text-out summarization call. It is
Process.monitor/1-ed, not Process.link/1-ed: a crashing delegate must
degrade to :skip, not crash the compacting agent (worker or orchestrator).
It is always stopped before this module returns, on every exit path.
compact/3 still blocks the compacting agent's own GenServer for the
duration — that's deliberate, unchanged from before (see
Planck.Agent.Hooks.Compactor's moduledoc on on_compacting/on_compacted).
What must not happen inside that GenServer callback is a raw receive
waiting on the delegate's PubSub events: the delegate is a real
Planck.Agent broadcasting :turn_start/:text_delta/:usage_delta/etc,
not just :turn_end, and any of those left unmatched by a receive done
directly in the compacting agent's own process would sit in its mailbox,
corrupting its own handle_info/2 dispatch once this call returns. So the
actual subscribe/prompt/await sequence runs inside a dedicated,
async_nolink Task instead — its mailbox and PubSub subscription are
simply discarded when it exits, regardless of what's left in it, the same
isolation Tools.call_agent's own await loop already relies on for this
exact reason. compact/3 blocks on awaiting that task, not on a receive
in its own process.