Skip to content

Architecture 4.2 ​

ContextChef compiles unbounded information into a bounded window. Every feature the library has — and every feature it will grow — sits on exactly one of five axes. This page is the map: one paragraph per axis, a link to the chapter that documents it, and the compile pipeline the five axes run through.

AxisThe question it answersOwned by
① SelectionWhat goes into the windowoverflow strategies, the Pruner, memory.selector
② PlacementWhere in the window it goesthe Assembler sandwich, the tail channel, the cache-stable prefix
③ PersistenceWhere it lives while it is out of the windowone context store
④ RetrievalHow the model reaches back outside the windowthe context tool and new_context
⑤ AdaptationWhat the provider's wire format wantsadapters

Overflow is not a sixth axis. It is information moving from ① to ③, kept reachable through ④ — which is why summarize / anchored / reset are selection policies, memory / notes / vfs / archive are persistence namespaces, and view / search are retrieval handles.

① Selection — what goes into the window ​

Two independent budgets: the conversation and the tool list.

History is bounded by an overflow strategy. The Janitor is the runner — it reads the budget, decides when the trigger is crossed, keeps the circuit breaker, and emits compress:* — while the strategy decides what actually leaves: summarize(), anchored(), server(), reset(), composed with chain() and background(). Pinned messages (pinned: true) are the one thing a strategy may not touch. See Overflow.

The tool list is bounded by the Pruner: prune by task, allowlist, or the two-layer namespace + lazy-loading architecture that keeps the compiled tool list byte-stable across turns. Memory selection is a third, much smaller budget — memory.selector picks which entries get injected each compile (Memory).

② Placement — where in the window it goes ​

The compiled payload is a sandwich: a stable, cacheable prefix (system prompt, tool schemas, skill instructions, the memory usage instruction) and a volatile tail stitched into the last user message (memory data, dynamic state, implicit context, announcements, the handoff notice).

The rule that makes prompt caching work is that nothing volatile may enter the prefix. Every library-owned tool schema is static — the context tool has exactly one enum, and it is a fixed list of commands, never a list of your memory keys. Everything that changes turn to turn goes through the tail channel instead. memoryPlacement, dynamicStatePlacement, skillPlacement and the announcement channel are the knobs; cacheAudit: true tells you when you got it wrong. See Memory, Skills and Tool Management.

③ Persistence — where it lives while it is out of the window ​

One substrate: addressed content with metadata. A StorageBackend moves bytes; a Store adds namespaces, context:// URIs, an access index and eviction. Four namespaces are in use — memory/ (durable facts, injected every compile), notes/ (the model's own scratch space), vfs/ (tool output too large to keep inline) and archive/ (spans compacted out of the window).

Before 4.2 these were three unrelated storage interfaces. They are now one, and the old ones (MemoryStore, VFSStorageAdapter, VFSMemoryStore, FileSystemAdapter) still work as deprecated wrappers. See Context store.

④ Retrieval — how the model reaches back outside the window ​

One model-facing tool. context covers all seven commands (view, create, str_replace, insert, delete, rename, search) across every namespace, and new_context lets the model close a window it is done with. chef.ownsTool(name) and chef.handleTool(call) are the single dispatch entry point for both, plus the legacy create_memory / modify_memory / recall_context trio.

ChefConfig.tools chooses which set compile() emits: 'legacy' (the default in 4.x — tool names are dispatch keys in your loop, so the default cannot flip in a minor release) or 'unified'. The mode also picks the session's vocabulary, so the prompt never names a tool the payload does not carry. See Context store.

⑤ Adaptation — what the provider's wire format wants ​

The same compiled IR becomes an OpenAI, Anthropic, Gemini or OpenAI-Responses payload: prefill, cache breakpoints, system-message hoisting, thought signatures and tool-call shapes are all adapter concerns. Input adapters (fromOpenAI / fromAnthropic / fromGemini) run the reverse direction and sanitize on the way in. See Adapters.

The compile pipeline ​

compile() runs a fixed, ordered list of phases. Each one has a declared contract, and slots fire at the boundaries between them.

start → transform-tool-results → handoff → overflow → inject
      → memory → skill → assemble → tail → adapt → audit → done
PhaseWhat it does
startcompile:start event, abort check, target resolution
transform-tool-resultstransformToolResult over every role: 'tool' message — before overflow, so the summarizer sees transformed content
handoffwhen the headroom falls into the handoff band, render the notice once per window
overflowbudget check, then the installed strategy; archive whatever was evicted
injectbefore-assemble handlers → <implicit_context>
memorysweep expired entries, apply the selector, derive the injection block and tool definitions from one store read
skillthe active skill's instructions, into the prefix or the tail
assemblesandwich assembly — system layers, history, guardrails, placements
tailtail stitch: skill, memory data, dynamic state, implicit context, announcements, anchor
adapttarget adapter, tools, server-managed payload fields
auditcacheAudit on the Anthropic target, plus the optional pipelineChecks invariants
donecompile:done event

Abort checks (compile({ signal })) sit after overflow, inject, memory, assemble and adapt — every boundary that follows an await which can take arbitrarily long.

Slots ​

Slots are the composition surface. Unlike events, which only observe, a slot handler participates in the compile: it can veto overflow, inject context, or rewrite the assembled messages. Handlers run in registration order and are awaited one after another.

SlotSignatureContract
before-overflow({ history, budget }) => void | falsereturn false to skip overflow for this compile
after-overflow({ history, result }) => voidresult is null when the phase was skipped
before-assemble(ctx) => voidctx.inject(text) adds a block to <implicit_context>
after-assemble(messages) => Message[]handlers chain — each receives the previous result
before-adapt(messages) => voidobservation, right before the adapter runs
after-adapt(payload) => voidobservation, before compile:done
typescript
chef.use('before-overflow', ({ budget }) => {
  // One more turn of headroom before the summarizer gets involved.
  if (budget.remaining > -2000) return false;
});

chef.use('before-assemble', async (ctx) => {
  ctx.inject(await retrieveSnippets(ctx.dynamicStateXml));
});

chef.use('after-adapt', (payload) => {
  metrics.record('compile', payload);
});

chef.unuse(slot, handler) removes one registration; registering the same function twice needs two unuse calls. The legacy onBeforeCompile and transformContext config hooks register on this same registry at construction, so they always run first — see Events & Hooks.

Non-negotiables ​

  • Mechanism, not policy. No combination of options is rejected. reset() without an archive is lossy and allowed; a handoff budget larger than the trigger is allowed. Silent-failure classes — a rewritten prefix, a dropped pinned message, a split tool pair — are reported through the audit and event channels, never blocked. pipelineChecks: true turns the reporting on in development.
  • Prefix byte-stability. Every library-owned tool schema is static and reference-stable, so payload.tools stays deep-equal between compiles. Volatile content only ever enters through the tail.
  • Zero forced migration in 4.x. Every renamed option keeps a deprecated alias that maps onto the new shape. Aliases are removed in 5.0.

Where to go next ​