Context compaction (SPEC.md §7F) — keep a long-lived agent under budget.
A long-lived agent grows its transcript until it overflows the model's window.
compactor returns a §8 :before-llm hook that summarizes the older transcript
and keeps a recent tail. It rides the seam that already exists: the loop applies
a :before-llm message rewrite by REPLACING the working transcript, and that
vector flows on into the run result and the conversation store. So compaction is
a pure messages -> messages helper and adds no loop behavior — it is the
canonical use of the :before-llm hook.
Three invariants (SPEC §7F):
:max-tokens the hook is a NO-OP — it returns nil and the run is
byte-identical to a run with no compactor.user turn, so no
tool message is ever orphaned from the assistant carrying its
tool_call_id.Messages are the same maps toolnexus.client builds: :role, :content, and
where present :tool_calls / :tool_call_id.
(client/create {:hooks {:before-llm (compaction/compactor
{:max-tokens 120000
:summarize my-summarizer})}})
Context compaction (SPEC.md §7F) — keep a long-lived agent under budget.
A long-lived agent grows its transcript until it overflows the model's window.
`compactor` returns a §8 `:before-llm` hook that summarizes the older transcript
and keeps a recent tail. It rides the seam that already exists: the loop applies
a `:before-llm` message rewrite by REPLACING the working transcript, and that
vector flows on into the run result and the conversation store. So compaction is
a pure `messages -> messages` helper and adds no loop behavior — it is the
canonical use of the `:before-llm` hook.
Three invariants (SPEC §7F):
1. At or below `:max-tokens` the hook is a NO-OP — it returns nil and the run is
byte-identical to a run with no compactor.
2. Tool-pair safety — the retained tail always begins at a `user` turn, so no
`tool` message is ever orphaned from the `assistant` carrying its
`tool_call_id`.
3. The leading system prompt (identity / soul / skills) is preserved verbatim;
only the body between it and the tail is summarized.
Messages are the same maps `toolnexus.client` builds: `:role`, `:content`, and
where present `:tool_calls` / `:tool_call_id`.
(client/create {:hooks {:before-llm (compaction/compactor
{:max-tokens 120000
:summarize my-summarizer})}})(compactor {:keys [max-tokens summarize keep-tail count-tokens
flush-to-memory]})Build a §8 :before-llm hook that compacts the transcript once it exceeds
:max-tokens.
Below budget it returns nil (a byte-identical no-op); above it, it replaces the transcript with
[leading system prompt, summary system message, (flush reminder?), ...tail]
splitting at a clean user boundary so tool groups are never broken.
:max-tokens compact only when the estimate exceeds this; at/below => no-op
(REQUIRED)
:summarize (fn [older] "summary") — produces the summary. MAY call an
LLM; the library makes no model call on the host's behalf
(REQUIRED)
:keep-tail keep at least this many tokens of the most recent tail
(default: half of :max-tokens)
:count-tokens (fn [messages] n) — token estimate (default estimate-tokens)
:flush-to-memory when true, inject a pre-compact system reminder telling the
model to persist durable facts through the §7E memory tool
before the head is summarized (off by default)
Build a §8 `:before-llm` hook that compacts the transcript once it exceeds
`:max-tokens`.
Below budget it returns nil (a byte-identical no-op); above it, it replaces the
transcript with
[leading system prompt, summary system message, (flush reminder?), ...tail]
splitting at a clean `user` boundary so tool groups are never broken.
:max-tokens compact only when the estimate exceeds this; at/below => no-op
(REQUIRED)
:summarize (fn [older] "summary") — produces the summary. MAY call an
LLM; the library makes no model call on the host's behalf
(REQUIRED)
:keep-tail keep at least this many tokens of the most recent tail
(default: half of :max-tokens)
:count-tokens (fn [messages] n) — token estimate (default `estimate-tokens`)
:flush-to-memory when true, inject a pre-compact system reminder telling the
model to persist durable facts through the §7E `memory` tool
before the head is summarized (off by default)(estimate-tokens messages)Cheap, deterministic token estimate: ceil(chars/4) over each message's JSON
serialization, summed.
An ESTIMATOR, not a tokenizer — exactness is the host's call, which is why
:count-tokens exists. The counts are not promised to be equal across ports:
each port serializes with its own JSON writer, and SPEC §7F pins the formula,
not the byte count.
Cheap, deterministic token estimate: `ceil(chars/4)` over each message's JSON serialization, summed. An ESTIMATOR, not a tokenizer — exactness is the host's call, which is why `:count-tokens` exists. The counts are not promised to be equal across ports: each port serializes with its own JSON writer, and SPEC §7F pins the formula, not the byte count.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |