Context, compaction and memory
Edit on GitHubEvery pass, the harness builds what the model is sent: the system sections, the tool descriptions, the history, and the newest input. Each of the parts an extension may change has a core default that costs nothing, and an override that names an extension. Installing an extension changes none of them by itself: a part is replaced only when config.toml names it.
What stays fixed
Whatever runs, core keeps:
- the system sections and their order;
- the tool descriptions the model is offered;
- the cache breakpoint: nothing above it changes from pass to pass;
- the newest input;
- the fit check, and the recording of what was sent.
The four parts
| Part | Core default | Override |
|---|---|---|
| Context builder | The harness's own assembly. | context = "<name>" |
| Context sections | None. | Declared by extensions; several compose. |
| Compactor | Summarise with the summariser role. | compactor = "<name>" |
| Memory | None: nothing is remembered. | memory = "<name>" |
context = "trim-old-tools"
compactor = "my-summariser"
memory = "notes"Context sections
A section is text for one pass, placed after the cache breakpoint. From state, it costs nothing:
export default defineExtension({
contextSections: [{ name: "recap", fromState: "recap" }], // the extension's own state key, read each pass
});Or rendered by code, asked under a short deadline on each pass:
contextSections: [{ name: "branch", render: (host) => "Work on the release branch only." }],Rust: implement ContextSectionHandler (async fn render(&self, at) -> Option<String>) and register it on the ExtCtx.
To see what reached a pass, run with RUST_LOG=orrery.kernel.context=debug: each pass logs its system sections and, under recalled, a preview of every section and recalled item after the breakpoint.
The context builder
A builder selected by context = "<name>" gets the branch since the last compaction, as entries, and answers edits by entry id:
contextBuilder: {
name: "trim-old-tools",
build(view, host) {
// view.entries: [{ id, kind, tokens, preview }], view.since: the last entry it was shown
const stale = view.entries.filter((e) => e.kind === "tool" && e.tokens > 2000).map((e) => e.id);
return stale.length ? [{ t: "replace", entries: stale, text: "[tool output trimmed]" }] : [];
},
},Edits are one of three kinds:
| Edit | Does |
|---|---|
{ t: "omit", entries } | Leave these entries out of this pass. |
{ t: "replace", entries, text } | Send text in their place. |
{ t: "insert", after, text } | Add text after an entry. |
The kernel applies the edits after the cache breakpoint only, and records them as a context.edit entry. The session therefore shows, and replays, what the model was actually sent.
Compaction
When the history no longer fits, the compactor summarises it. The model may also ask for it early, with the core tool context.compact:
context.compact { upto?, focus?, instructions? }uptois how far to compact;focussays what to keep;instructionsis text added to the summariser's prompt for that one compaction.- A compaction is a checkpoint. Nothing before it reaches the model again; it stays in the session. Its cost is kept with it and counts toward the turn.
The default compactor is the summariser role. Its prompt, model and effort are the user's, set in config.toml:
[agents.summariser]
prompt = "Summarise the conversation so the work can continue: decisions, open questions, what changed."
model = "claude-haiku-5-5"
[roles]
summariser = "summariser"For the summariser and compactor roles the agent's prompt replaces the harness's base prompt; for other roles it is added to it.
An extension compactor, selected with compactor = "<name>", answers the same call:
compactor: {
name: "my-summariser",
compact({ plan, upto, focus, instructions }, host) {
/* return the Compacted reply for plan */
},
},Memory
Memory is three core tools. Core defines them; the memory extension chosen by memory = "<name>" implements them:
| Tool | Does | Checked under |
|---|---|---|
memory.save { text, scope } | Keep something. Answers its id. | mem.write(<scope>) |
memory.search { query, scope? } | Find what was kept. | mem.read(<scope>) |
memory.forget { id } | Drop it. | mem.write(<scope>) |
With no memory chosen, the three tools are not offered. The harness ships no memory implementation: an extension provides one.
memory: {
name: "notes",
save({ text, scope }, host) { /* … return an id */ return "n1"; },
search({ query, scope }, host) { return []; },
forget({ id }, host) {},
// recall({ query }, host) — optional: automatic recall at turn start; none recalls nothing
},Cost and limits
- By default none of this costs anything.
- A builder costs one round trip per pass, sending only the previews of entries it has not seen.
- A section from state costs nothing; a rendered one costs one call.
- The compactor costs one model call at overflow or on request.
- An extension's model calls per turn are capped by
limits.modelCalls(8 by default); the deadlines for builders and sections are in Config keys.