The context engine¶
This document specifies how a run's model request is assembled: which content goes on which side of the prompt cache boundary, in what order, under what token budget, with what trust labels, and what yields when the budget binds. It expands Section 11 of the engineering plan, sits under Milestone 7, and is recorded as ADR-0020.
Scope: assembly. What to remember is specified in memory formation; what to recall is specified in memory retrieval. This document owns the container both of those write into, and the guarantees that container makes to everything else.
Why assembly is its own problem¶
Section 11 currently describes the context builder as a list of seven things to concatenate. That understates it. The builder is the component that every other subsystem hands its output to, and it is the only one that can violate all of their guarantees at once:
- It is the cache boundary. Section 10.1 fixes a prompt-stability invariant — a byte-stable prefix built once per session — and names the context builder as the thing that enforces it. Nothing else can. A single volatile byte in the prefix converts the cheapest request in the system into the most expensive one, silently, for the rest of the session.
- It is the trust boundary in practice. Section 11.2 assigns every item a
TrustLevel, but a label only means something at the moment content is rendered into a prompt. Compaction, summarization, and truncation all pass through the builder, and each is an opportunity to strip a label off content that still carries its original risk. - It is where scarcity is resolved. Memory, history, tool definitions, tool results, and working state all want the same tokens. Every other subsystem sizes its own appetite; only the builder decides who actually eats.
The failure mode is not "the prompt was wrong." It is that all three failures above are invisible in the output. A cache-thrashing session returns correct answers at ten times the cost. A laundered label returns a correct-looking answer derived from attacker-controlled text. A budget overrun returns a provider 400 twenty minutes into a long run. This spec optimizes for assembly that fails loudly at build time rather than expensively at run time.
What the context engine must respect¶
These are fixed by earlier decisions and are not re-litigated here.
- Prompt-stability invariant (Section 10.1). The cacheable prefix — platform policy, agent instructions, tool definitions — is built once per session and kept byte-stable. Volatile content goes in the user turn.
build()is a port (Section 7).ContextBuilder.build(run, checkpoint, agent, principal) -> ModelRequestis the existing signature and this spec does not change it. Everything below is a statement about what that function must do.- The session owns history, the run owns its working conversation (Section 27.4, ADR-0009). A new run seeds from the session-history projection built from events, not from the previous run's checkpoint.
- Provider-opaque reasoning items never cross a run boundary (Section 6.6, ADR-0007). They live in the checkpoint for the life of a tool loop and are replayed verbatim, never authored, summarized, or logged.
- Private chain-of-thought is never requested or stored (Section 11.4, ADR-0006 as amended). Only messages, actions, evidence, concise decision summaries, and structured working state.
- The agent version is pinned per run (Section 6.1). Agent instructions cannot drift mid-run, which is what makes a session-scoped prefix possible at all.
- Memory renders under a fixed contract (retrieval spec, stage 8). The
<memory>block's ordering, banding, and attribution rules are that spec's; this document places the block and pays for it.
Two regions and one rule¶
Every item in a request belongs to exactly one of two regions, and the assignment is a property of the item's type, decided at design time, never at run time.
Region A — the frozen prefix. Built once per session and byte-stable for the life of the prefix epoch. It carries platform policy, agent instructions, the persona row, the filtered tool definitions, the session-open memory snapshot, and the framing text that tells the model how to treat everything downstream.
Region B — the turn body. Rebuilt on every request. It carries the compacted history summary, recent conversation items, the working-state block, in-turn recall and correction lines, tool calls and their results, runtime metadata, and the current user message.
The rule is one sentence: if it can differ between two requests in the same session, it is in Region B. Not "if it usually doesn't change" — if it can. The current date is the canonical trap. It is in Section 11.1's assembly list, it feels like configuration, it changes at most once per session, and putting it in the prefix breaks every session that crosses midnight in a way that no test written before 23:00 will catch.
Assembly order is fixed and total:
| # | Region | Content | Trust |
|---|---|---|---|
| 1 | A | Platform policy and hardline rules | PLATFORM |
| 2 | A | Framing: how to treat memory, tool output, and untrusted spans | PLATFORM |
| 3 | A | Agent instructions (pinned AgentSpec version) |
TRUSTED_CONFIGURATION |
| 4 | A | Persona row (pinned persona revision) | TRUSTED_CONFIGURATION |
| 5 | A | Tool definitions (filtered, canonically serialized) | TRUSTED_CONFIGURATION |
| 6 | A | Deferred tool index, if any | builtin entries TRUSTED_CONFIGURATION; others EXTERNAL_UNTRUSTED |
| 7 | A | Skill catalog (pinned at session open) | per entry |
| 8 | A | Session-open memory snapshot | MEMORY |
| 9 | B | Compacted history summary, if any | PLATFORM (see below) |
| 10 | B | Retained conversation items and completed active exchanges, oldest to newest | per item |
| 11 | B | Loaded skill bodies, in load order | per skill |
| 12 | B | Working-state block | per entry |
| 13 | B | In-turn recall and correction lines | MEMORY |
| 14 | B | Runtime metadata: current date, principal scope, surface; the origins browser.navigate accepts when the plan offers it |
PLATFORM |
| 15 | B | A new trailing user input, if present | USER |
Completed exchanges include the active run's original user message once it has tool calls and results. Fresh rows 11–14 follow those exchanges; they must not be inserted back before the original user message on every step. Recall and corrections are recomputed as before, and remain in their memory-trust envelopes. An unanswered trailing user message (including supplemental input on resume) stays last. Tool-call/result batches and their latest opaque continuation retain their order before the fresh rows. This makes the portable history grow without freezing context updates (ADR-0139). It changes neither selection nor budgets: active items are still never-yield inputs, not candidates for the history cut.
Rows 7 and 11 are added by skills.md, which owns their content, their caps, and their trust derivation. Row 4 is added at Milestone 22 by persona-surface.md, which owns its content, its cap, its promotion rules, and the row's rotation behavior; an empty persona renders no bytes at all, so a session without one reproduces the three-row instruction prefix exactly. Row 6 is added by ADR-0123; an empty index renders no bytes either. They are listed here because assembly order is fixed and total, and a table that omits four of its rows is neither.
Platform policy comes first because it is the only content that must be read before anything that might try to override it. The current user message comes last because recency is the strongest positional signal available and the turn's actual request should hold it.
Enforcement is a test, not a convention¶
An invariant maintained by developers remembering it is not an invariant. Three mechanisms, in order of how much they catch:
A region-assignment table in code. Each context item type declares its region as a class attribute. The builder asserts on assembly; an item with no declared region is a build failure, not a default. Adding a new item type therefore forces the decision rather than allowing it to be skipped.
A recorded prefix hash. Every request records prefix_sha256 on
model.request.started. Prefix stability is then an observable property of
production traffic rather than a hope: a session with more than one distinct prefix
hash per epoch is a defect with a session id attached.
A scripted stability test in CI. Drive a fifty-turn session against the fake
provider (Section 10.3) with the clock advanced across a day boundary, tools
partially revoked mid-session, memory written and corrected mid-session, and a
forced compaction. Assert exactly one distinct prefix_sha256. This is a hard gate
on the milestone.
Authorization changes do not rewrite the prefix¶
Section 11.1 says filter tools by agent configuration, principal authorization, policy profile, and runtime environment. Three of those are session-stable. The fourth is not: a scope can be revoked while a session is open, which would change the tool list, which would rewrite the prefix.
Resolve it by separating the advertisement of a tool from the authorization to
call it. The tool set is resolved once at session open and pinned in the context
plan. Two resolution-time rules narrow the set before it pins. When the pinned
skill catalog is empty, skills.md directs the planner not to
advertise skill.load, because a control tool with no valid argument only
invites guessed names; tool.call is likewise advertised only while the plan
defers something (ADR-0123). An operational Email session pins no tools at all,
because its typed tasks send none to a model (ADR-0104). A tool whose authorization is later revoked stays in the
prefix and is
denied at call time by the policy engine (Section 9), which is where the security
boundary actually lives — a tool definition in a prompt grants nothing. The denial
is structured and the model is told plainly that the capability is no longer
available, which is information it can act on.
This is deliberately the safe direction to be wrong in. The failure it permits is a wasted model call that ends in a clean denial. The failure it prevents is a prefix rewrite, and a prefix rewrite on every permission change is both a cost regression and a covert channel — the shape of the tool list would otherwise leak authorization changes into the cache timing of an unrelated session.
Prefix epochs¶
ADR-0172 also stores browser_profile_id outside the prompt. At a new run's
unpinned admission boundary, a changed selected profile rotates the epoch with
reason browser_profile_changed, even with unchanged scopes. Ordinary active
runs retain their pins. The missing-access preflight exception has performed no
model work: a verified owner connection resolves its question, clears its tool
pins and provider continuation, and resumes the original run once.
ADR-0094 adds a narrow
exception to session-long authorization pinning: before a new run initializes
its tool pins, newly granted effective run scopes rebuild the plan and emit
context.epoch.rotated with reason run_authority_changed. This lets an
authenticated owner reply to a restricted scheduled briefing in the same
conversation. Rebuilding applies the new run's permissions and the pinned
agent's configuration; it never grants the scheduled run additional scopes.
The plan stores authority_scope_hashes, a sorted tuple of SHA-256 scope hashes
outside the prompt. A legacy plan without that field
rebuilds once at the next unpinned run. An initialized empty tool set is pinned,
as are legacy nonempty pins and pending calls: retries and approval resumes
must not refresh authority or rewrite their advertised tools. A scheduler's
seed checkpoint with no pins or pending calls can still build its first plan.
Scope reduction alone preserves the prefix even across runs; revocation
continues to be enforced at call time without rewriting its prefix.
Some changes genuinely cannot be absorbed. Routing the session to a different model or provider invalidates the prefix by construction: tokenizers differ, tool serialization differs, and Section 10.1 notes that changing tool definitions or thinking parameters invalidates downstream cache anyway. An operator revoking a capability for a security reason may need it gone from the prompt immediately, not merely denied at call time.
These start a new prefix epoch: the plan is rebuilt, epoch increments,
context.epoch.rotated is emitted with a reason, and the full prefix is re-cached
at known cost. Epochs are explicit, logged, and counted. Epochs per session is a
tracked metric and its target is 1.0; a deployment averaging materially more than
one has a configuration problem, and because the counter exists, it has one
visibly.
One rotation reason rebuilds nothing. Before
ADR-0134, a plan's hash
covered each tool's scope set in whatever order its worker's hash seed gave
it, and its plan event recorded the scopes as they stood when it was written.
When such a plan's hash matches its prefix rendered with those recorded
orders, and nothing else about the plan changed, the planner appends the same
plan as a new epoch with its canonical hashes and reason
prefix_hash_canonicalized. It does not rebuild the plan under a run that
may be parked on an approval, and the provider receives the same bytes, so
nothing is re-cached.
The history cache window¶
Region A is cached by the plan's after_system and after_tools breakpoints.
Region B is cached by its third, after_history_prefix: the rolling window
Section 10.1 asks for. The builder places it on every request as up to two
markers, each naming the last conversation item its cached prefix includes
(CacheBreakpoint.through_item):
- The carried-history marker closes the compacted summary and the retained history. This boundary remains reusable even when fresh context changes and the new run has not yet completed an exchange. A first run carries nothing and has no such marker.
- The run marker closes the longest prefix the run's next step repeats: the completed conversation, stopping before fresh step context or a replayed provider continuation. The runtime replays only the latest turn's reasoning items (ADR-0007), so the next step no longer carries these, and every envelope nonce after them moves with its index.
A marker past either point would write a cache entry that no later request
reads, paying the provider's write premium on every request for nothing. Both
markers follow the prefix breakpoints in priority order, so a four-breakpoint
provider sends all four and a smaller budget drops the run marker first
(model-gateway.md). A provider finds an earlier entry only
within its lookback, about twenty blocks on Anthropic, so a run that appended
more than that re-caches the carried history once on the next run's first
request. The window is a provider cache marker, not prompt text: it is outside
prefix_sha256, it changes no byte the model reads, and a plan persisted
before it gains the window when loaded, without rotating its epoch.
The plan also chooses how long the provider keeps each entry (ADR-0132). Both
prefix breakpoints carry one TTL, fixed when the plan is built. A scheduled
occurrence gets the one-hour TTL, because its single autonomous run waits on
children, slow tools and the async queue. Every other session gets the default,
delegated children included. The history window keeps the default in every
session: its markers move every step, so a one-hour write there would pay the
premium on each request, and a shorter entry after a longer one is the order
the provider requires. The TTL is not prefix identity either. It never rotates
an epoch, and a plan built before ADR-0132 keeps the default until a rotation
for another reason rebuilds it; a prefix_hash_canonicalized epoch copies the
plan and keeps its breakpoints.
Session identity and OpenAI history lookback¶
Both builders put session- followed by the first 32 hex characters of the
SHA-256 of canonical [tenant_id, session_id] on CacheHints.session_key.
Runs, steps and epochs share it; another tenant or session does not. Excluding
the epoch preserves reusable material across plan rotations, while the changed
prompt still invalidates the affected prefix. Separating sessions sacrifices
cross-session Region A cache reuse for isolation and older-model routing; on
Astra the key separates accounting rather than improving automatic routing.
For each history marker the builder also supplies through_input_item: the
latest user or tool-result item at or before through_item, if one exists.
OpenAI uses this alternative because its implicit lookback can find an earlier
input ending after the explicit marker moves on the next run. Anthropic keeps
the full through_item boundary. This is a cache annotation, never a rewrite
of messages, trust envelopes, context order, or continuation lifetime.
Recall is recomputed per step and its timestamp, corrections and selected facts can change. Working-state edits, skill loads and date changes also change the fresh context after completed exchanges. The run marker protects the growing portable prefix before those rows, and the carried-history marker additionally protects prior runs. Tool-result truncation or history compaction can change history itself and legitimately lose that hit. Never freeze corrections, suppress working-state updates, or exceed budgets to improve caching. Tests compare the actual Responses inputs, including the first changed byte at the latest reasoning item when the preceding body is stable. See ADR-0135.
The persona row¶
Milestone 22 adds one Region A row between the agent instructions and the tool
definitions: the persona row, rendered from a persisted, versioned,
principal-scoped document at TRUSTED_CONFIGURATION. A session pins one
persona revision when its plan is created; a mid-session edit persists
immediately and takes effect at the next context plan as a single epoch
rotation with reason persona_changed — exactly the behavior an
agent-instruction edit already has. An empty persona renders no bytes, so
every session without one keeps today's prefix byte-for-byte.
persona-surface.md owns the document's data model, the
promotion rules that feed it, the cap, and the gates that watch all of it;
this document owns only the row's place in the assembly order and its region.
The budget allocator¶
Section 11.3 specifies ContextBudget as seven integers and requires an allocator
rather than string concatenation. It does not say where the integers come from or
what happens when they do not fit. Both are decided here.
Only history scales with the window¶
Total capacity is min(model.max_context_tokens, policy_cap). The output reserve
comes off the top as a subtraction, not a share — a request that fits but
leaves no room to answer is a failed request that costs full price. What remains is
divided among classes, and the division follows one principle:
Every class except history is capped absolutely, and only history scales with the context window.
This generalizes the argument the retrieval spec makes about the memory snapshot. Prefix content is read on every request of the session, so its cost is attention paid repeatedly, and attention degrades with the absolute number of items competing for it — not with the fraction of the window they occupy. A larger window is not a reason for a longer policy document, more tools, or a bigger snapshot; those get worse as they grow, and the window has nothing to do with it. History is the one class whose value genuinely increases with size, because more history is more of the actual conversation. So history takes the remainder and everything else takes a ceiling.
| Region | Class | Cap | Scales | Yields |
|---|---|---|---|---|
| A | Platform policy | 2,000 | No | Never — fails at plan time |
| A | Agent instructions | 4,000 | No | Never — fails at plan time |
| A | Persona row | 30 entries / 2,000 tokens | No | Never — fails at plan time |
| A | Tool definitions | 30 tools / 9,000 tokens | No | Only at an epoch boundary |
| A | Deferred tool index | 40 entries / 2,000 tokens | No | Only at an epoch boundary |
| A | Skill catalog | 20 skills / 1,500 tokens | No | Only at an epoch boundary |
| A | Memory snapshot | 40 items / 1,500 tokens | No | Only at an epoch boundary |
| B | Skill bodies | 2 loaded / 6,000 tokens | No | Never — the load fails instead |
| B | Working state | 1,000 | No | Never |
| B | Knowledge passages | 3 passages / 3,000 tokens | No | First |
| B | In-turn recall | 2,000 | No | Second |
| B | Tool results | 25% of body | Partly | Third |
| B | History | remainder, floor 8,000 | Yes | Fourth |
| B | Current user message | uncapped | — | Never |
| — | Output reserve | 8,192 or the model's default | No | Never — subtracted first |
Tool definitions carry an item cap as the primary limit, exactly as the snapshot does and for exactly the same reason: selection accuracy degrades with the number of candidates, not with their token weight. Thirty tools is already generous; a deployment that needs more needs tool filtering or skills (Section 30.4, where only skill metadata enters ordinary context), not a bigger allowance.
The token cap is the secondary limit. It bounds what that item cap may cost, and
it remains an admission check: a discovered definition that would take the prefix
past it is skipped, as the selection rule below states. It is sized so that an
ordinary roster reaches the item cap first. Six thousand tokens was not: the
owner's own configured roster — People, Email mode, scheduling, web and
workspace — reached twenty-six tools and 5,973 tokens, so every discovered tool
was skipped while four of the thirty slots stood empty. Calling shipped tools the
owner could never see, and no error said so.
ADR-0105 raises the class to 9,000
tokens so that the item cap binds first, as this section always intended. The
argument above still holds against raising the item cap. The skill catalog
carries an item cap for the same reason and is capped at twenty;
skills.md argues that number and the 6,000-token body class beside
it, which never yields because a third skill.load fails instead.
The item cap then bound in production. Email unsubscribe brought the owner's
configured roster to twenty-eight tools, and name order left
mcp.bland_read.list_calls outside the thirty.
ADR-0124 takes
workspace.list_files out of the default agent's roster, which leaves exactly
three discovered slots for the calling tools. A configured tool past the
thirtieth slot is cut as silently as a discovered one, so the production-shaped
roster gate enables every flag that production enables and that changes the
default roster, and gains each new one when production activates it.
The tool filter this section prescribes is the deferred tool index
(ADR-0123). A deferred tool
is resolved, authorized and pinned exactly like a defined one, but the prompt
carries a one-line index entry instead of its definition: the name, its
parameter names with optional ones marked, and the first sentence of its
description. The model reaches it through the control tool tool.call, which
the tool pipeline unwraps before resolution, so validation, policy, approval,
idempotency and events all run under the deferred tool's own name; invalid
arguments return that tool's input schema. A tool is advertised by its
definition or by its index entry. The index is its own Region A class,
forty entries and 2,000 tokens, and the prefix ceiling rose by exactly that cap.
An entry lists required parameters in the order of the schema's required
array and optional ones by name, never in the order of the schema's
properties: the builder re-renders the prefix from the specifications on the
plan event, and PostgreSQL jsonb keeps array order but not object key order.
After authorization and session-environment filtering, tools explicitly named in
AgentSpec.enabled_tools receive definitions first, in first-occurrence order,
except the ones the agent's configuration names as deferred. Discovered tools
follow reads first: tools whose side effect is NONE, WORKSPACE_READ or
NETWORK_READ before any that change the world, then by server or device, then
by name. A deferred tool costs at most one extra model step, and a read is
usually a request's first step, so reads keep their definitions. Discovered
definitions must fit both the item and token caps; explicitly enabled
capabilities still fail at plan time if they exceed the token cap. The selected
set is then sorted by name for stable rendering. Adding an MCP account must not
evict an explicitly enabled web, clock, or workspace capability merely because
its name sorts earlier.
In a session bound to a website profile, browser.navigate,
browser.observe and browser.act are required definitions (ADR-0130): they
rank ahead of every other candidate, neither the agent's deferral list nor
overflow moves them to the index, and a plan that cannot define them fails at
plan time. What they displace moves to the index in the order above.
When anything is deferred and the agent enables tool.call, tool.call takes
a definition slot, and every candidate without a definition becomes an index
entry: the configured deferrals first, then the overflow in the order above.
What fits in neither is skipped, and its name is recorded on the plan event as
skipped_tool_names. An agent without tool.call defers nothing and records
its skips the same way, so no tool leaves the roster silently. Builder version
context-builder@12 treats a context-builder@11 plan as current, because it
renders the same bytes: re-planning it could move the selection under a run
parked on an approval. Earlier versions rebuild through the ordinary logged
epoch rotation, so existing sessions recover the configured capabilities
without replacing their history. A snapshot containing People context is
retained even when it contains no ordinary beliefs; the epoch rotation also
rebuilds older plans that omitted such snapshots.
The token side of the tool-definition cap measures the conservative larger form
of the model-visible provider contract: name, description, and input schema
plus provider framing. Provider serialization omits redundant generated titles
and empty default annotations while retaining every validation keyword and
meaningful non-empty default. The complete pinned ToolSpec still participates in the
prefix hash and replay identity, but its output schema, policy classification,
timeouts, and execution limits are not sent to the model and therefore do not
consume this prompt class. Its required_scopes set serializes sorted: a set
iterates in an order that follows each process's hash seed and can change
whenever it is copied, so an unsorted dump could give one plan a different
hash in another worker
(ADR-0134). A session-bound capability is also a runtime-environment
filter: when trusted session metadata has no selected binding, its definitions are
absent before the plan is pinned rather than advertised as unusable tools.
Knowledge passages are a separate class from in-turn recall rather than a share of
it, and knowledge-documents.md argues both the split and
the number. A passage is a verbatim quotation the model may cite, so it is three
times the weight of a belief and cannot be trimmed by sentence; the class is capped
at three passages because a document that answers a question usually answers it in
one or two, and it yields before recall because a corpus is re-queryable by an
explicit knowledge.search while the beliefs in a snapshot are not.
The prefix classes sum to a hard ceiling of 22,000 tokens — 15,000 before Milestone 22's persona row, whose 2,000-token class raised the ceiling by exactly its own cap, the same move the skill classes made, 17,000 before ADR-0105 added the 3,000 tokens it gave the tool class, and 20,000 before ADR-0123 added the 2,000 of the deferred tool index. If a plan exceeds it, the session fails to open with a structured error naming the offending class. It does not silently truncate the agent's instructions. A truncated system prompt is an agent that behaves subtly wrong forever, which is far worse than a session that refuses to start with a clear reason.
What history selection retains¶
The yield order below says which class gives up tokens first. It does not say which conversation items were in the request to begin with, and that is the decision that determines whether two runs with the same input produce the same prompt. Selection happens at two moments, on two different inputs, and they are two different functions.
At run seed, the input is a log prefix, not a projection. The session-history
projection is a live read model that advances from its watermark on a timer;
asking it for "the session's history" returns whatever has been applied at the
moment the question is asked. seed_checkpoint is called twice — once when the
application service creates the run, and again when CheckpointRepository.latest
returns None because the Milestone 2 dispensability gate deleted the run's
checkpoints — and the two calls can be hours and a deploy apart. Reading the
projection as it stands would make the second seed a different conversation from
the first, which is precisely the failure that gate exists to catch and would
instead be causing.
So the seed reads the projection cut at a fixed sequence: the session sequence
of the user.message.created event the run answers, recorded on the run row when
the run is created.
# additive column on the runs table
runs.seed_event_sequence BIGINT NULL -- session sequence of the seeding
-- message; history is every item
-- strictly below it
The column is written in the transaction that appends the seeding event and inserts the run row — the transaction that already allocates the sequence, so it costs nothing. It is nullable for the child runs of Section 27.6, which seed from a parent's concise instruction rather than from session history and therefore select over an empty input, which is the same function rather than a branch in the caller.
Projections are already required to be deterministic over a log prefix. Pinning the prefix is what turns that property into a guarantee about seeding, and it needs no gate of its own: the dispensability gate is the test, and it only tests anything because the cut is fixed.
Legacy tool results get deterministic excerpts before selection (ADR-0137). New
results carry the tool pipeline's persisted context_content excerpt beside their
canonical content. Both context builders select that excerpt on a deep copy
before estimation or provider rendering, and clear the alternate field in the
request. The checkpoint and event retain canonical content for source receipts. For a carried result
whose serialized content still exceeds output.inline_maximum_bytes, assembly
uses the same head/tail excerpt algorithm and references its original event.
It preserves the result's trust and any first artifact reference, and does not
modify the checkpoint or source event. Selection is independent of later tool
results; a deployment or operator limit change can change these bytes once.
The aggregate tool-result budget and its oldest-first yield still apply after
this normalization, including to active results from a resumed older run.
legacy_tool_excerpts records this normalization in the pressure event.
At assembly, the retained set is a suffix, never a subset. History is selected as a contiguous tail of the ordered item list — one cut index, everything at or after it in, everything before it out. Not a relevance ranking over past turns, and not "the important ones". Three reasons, in order of how much they cost to get wrong.
A non-contiguous selection produces a conversation with holes, and a model reading a hole does not see a hole. It sees a conversation in which the thing that filled the gap never happened, and reasons confidently from that. The absence is invisible in the transcript and expensive in the answer.
Contiguity also makes tool-pair atomicity a property of the cut rather than a pass that runs after it. With a suffix there is exactly one boundary that can split a pair, so the repair is one adjustment to one index. With a ranked subset any pair can split, and the repair changes the token total, which can require a second repair.
And a relevance-ranked history would be a second retrieval system — a second ranker, a second set of tuning parameters, a second set of failure modes — beside the one this corpus already has. In-turn recall exists precisely to pull back the older thing that matters. History is recency; recall is relevance. Collapsing them makes both untestable, because a missing turn is then either a selection defect or a ranking miss and no test can tell which.
The cut is the largest suffix that fits. Four rules compute it, and each is total:
- Scan backward from the newest item, accumulating estimated tokens, and stop at
the first item whose inclusion would exceed
budget.history_tokens. The floor is applied by the allocator before the predicate runs, so the predicate reads one number and never re-derives it. - The cut never falls earlier than
replaced_through_sequence. Items the summary already covers are represented at position 7; admitting them again states the same turns twice in two voices, and the paraphrase and the original will disagree about emphasis. - If the cut splits a tool pair, it moves later, past the orphaned result, so the pair is excluded as a unit. Never earlier: admitting the call would add tokens to a set already at its limit, and the pair is atomic in both directions.
- The never-yield items are not subject to the cut at all. The current user message, the working-state block, the correction lines, and the pending pairs of an active loop are assembled first and their cost is subtracted before the scan begins. If they alone exceed the class, the request fails — the same rule the prefix follows, for the same reason.
def select_history(
items: Sequence[ConversationItem], # ordered, oldest first
summary_floor: int, # replaced_through_sequence, or 0
history_tokens: int, # budget.history_tokens, floor applied
estimator: TokenEstimator,
model_id: str,
) -> int: ... # cut index; items[cut:] are retained
summary_floor is an event sequence; the return is an item index. The two
are different units and the function never compares them to each other. Each
ConversationItem carries the sequence of the event it was built from, and the
floor is applied by comparing that to replaced_through_sequence: an item at
or below the floor has been replaced by the summary and is not a candidate. The
index the function returns is then a position in items, derived after the
floor has been applied, never a sequence number in disguise.
It returns an index, not a list. An index can only describe a suffix, so the contiguity rule is carried by the return type instead of by a test somebody has to remember to write.
The estimator is pure. TokenEstimator.estimate is permitted to be
approximate and is now also required to be a pure function of its arguments — no
clock, no sampling, and no cache that can change the number it returns rather
than the time it takes to return it. An approximate estimator puts the cut in a
slightly different place than an exact one would, which is a tuning question. A
non-deterministic estimator puts the cut in a different place on two calls with
the same input, which is the failure this whole subsection exists to prevent.
Yield order under pressure¶
When the assembled body will not fit, the builder yields in a fixed order, taking the cheapest and most recoverable loss first:
- Knowledge passages drop, lowest-ranked first. They are dropped whole and
never truncated, because a passage shortened to fit is a misquotation of a
document the model is about to cite. They go first because the corpus is still
there: an explicit
knowledge.searchre-reaches it in one tool call. - In-turn recall trims to its floor. It is the marginal addition, it was
selected against a relevance floor that can simply be raised, and it is the most
recoverable thing in the request — the agent can call
memory.searchexplicitly if it turns out to need it. - Tool results truncate to pointers, oldest first. The full result is already in the event log and, above the inline threshold, in the artifact store. What remains is a typed pointer with the byte count and reference, so the model can see that content exists and ask for it rather than concluding it never existed.
- History compacts. Deliberately last: it is the only step that costs a model call on the critical path, and the only one that loses information the run cannot cheaply re-fetch.
Four things never yield, and the builder fails rather than dropping them: the current user message, the working-state block, the correction lines that override the frozen snapshot, and the pending tool-call/result pairs of an active loop.
Tool call and result items are atomic budget units. A ToolCallItem without its
ToolResultItem is not a smaller request, it is a malformed one — both providers
reject the sequence — and the failure surfaces as a provider 400 that reads like a
transport bug. The allocator treats the pair as one indivisible item and a
validator rejects any assembled request containing an orphan.
Pressure is resolved by a write, never inline¶
The single most important property of the builder is that build() is a pure
function of its inputs. Called twice on the same checkpoint, it returns
byte-identical bytes. That is what makes retries safe, makes the stability test
possible, and makes a request reproducible from the log.
Compaction is a model call, and model calls are not pure. So compaction is not
something build() does. The loop measures pressure before the call; if the body
will not fit, it invokes the compactor, which writes a new checkpoint carrying
the summary, and then calls build() again on that checkpoint. Section 6.9 already
lists context compaction as a checkpoint trigger; this is the reason it is one.
The separation is what keeps assembly testable. A builder that summarizes inline is a builder whose output cannot be predicted, cannot be diffed, and cannot be asserted byte-stable — and prefix stability is exactly an assertion about bytes.
The token estimator¶
Section 11.3 permits an approximate estimator behind a replaceable interface. Four constraints on it:
It is conservative by construction. Over-estimating wastes headroom. Under-estimating produces a provider context-length error mid-run, which on a long async run means losing real work. The estimator rounds up, and the allocator holds a further safety margin (5% by default) on top.
It never covers the prefix. The prefix is measured exactly, once, at plan time by rendering it and counting, and the count is stored in the plan. Only the elastic body is estimated per request. This confines estimator error to the part of the request that has slack, and it means prefix-size failures are caught at session open rather than on turn forty.
It is per-model. Tokenizers differ, and the interface takes a model id. A shared character-ratio heuristic is an acceptable first implementation; a wrong one applied uniformly across providers is not.
It reconciles against reality. Every model.response.completed carries actual
usage. The estimator maintains a per-model correction factor from the observed
ratio, which converges quickly and, more usefully, makes estimator drift a visible
number rather than a mystery. The factor resets when the model changes.
It counts attachments (ADR-0120). An attachment reference serializes to a few
dozen bytes, but the adapter sends the file. The estimator therefore adds, for each
image or file reference in an owner-written user message that passes the shared
per-item limits in agent_core.model.attachments, that module's fixed estimate: a
flat figure per image, a figure per PDF page, and the inlined length of a text file.
The content estimate is capped at the shared request-wide attachment token budget;
labels are counted for every reference, including markers. Model capabilities can
only remove content, so this remains an upper bound on what the adapter sends.
Compaction¶
Section 11.4 gives the first compactor a correct retention list — current goal, explicit constraints, unresolved questions, tool results needed downstream, source event ids — and says not to build anything sophisticated before the basic loop works. Agreed. What it does not say is how compaction coexists with a prefix that must never be rewritten, and that is where naive implementations break the invariant without noticing.
Compaction only ever touches Region B¶
The prefix is not compactable content. It contains no history. A compactor that "summarizes the conversation so far" and rebuilds a system message from the result has just rewritten the prefix, and the resulting cache miss will be attributed to whatever change shipped that week rather than to the compactor.
Compaction replaces the oldest end of the body with a summary item that sits at position 7 in the assembly order. Section 10.1 places a rolling cache breakpoint over the last few non-system messages; compaction invalidates that rolling window and nothing above it. The cost of a compaction is therefore bounded and known in advance: re-cache the history window, keep the prefix.
Untrusted content is elided, never paraphrased¶
This is the load-bearing rule and it is a security rule, not a quality one.
Summarization launders trust labels. A conversation containing a tool result marked
EXTERNAL_UNTRUSTED — a fetched web page, an inbound email, a third-party API
response — gets compacted into a paragraph of fluent narration, and that paragraph
is not labeled anything. It reads as the platform's own account of what happened.
Every downstream defense in Section 11.2 depends on the label, and the label is now
gone. Worse, the injection that was inert while quoted as data is now embedded in
text the model has been told to treat as a trustworthy record of the session.
So: the summarizer runs on trusted content only. Untrusted spans are not summarized. They are replaced with a typed pointer:
The pointer states that content existed, what it was, how large, where it lives, and that it was untrusted. The model can ask for it, and re-fetching it returns it inside its envelope with its label intact. Nothing is lost except the ability to launder it.
The same rule protects the trusted side less dramatically but usefully: every
summary carries the union of its source_event_ids, so any statement in a summary
can be traced to the events that produced it — the same provenance discipline
memory formation applies to beliefs.
Summaries are bounded and monotone¶
Summaries of summaries lose provenance and drift. Cap summary depth at 2: raw items compact into a summary, and summaries may merge once. Beyond that, the run has outgrown its window and should be escalated — a child run with its own budget (Section 27.6), or a handoff — rather than compressed a third time into something whose relationship to what happened is no longer checkable.
Merging summaries takes the union of source event ids and the union of retained constraints. Constraints are never merged away; if the retained constraint set alone will not fit, that is a failure, not a compaction target.
Trust labeling and rendering¶
Section 11.2 requires a trust classification on every context item and states two rules: external content must be represented as data rather than instructions, and a tool result must never redefine policy, grant permissions, or change approval requirements. Both are properties of how content is rendered.
Every non-platform region is enveloped¶
Content below TRUSTED_CONFIGURATION renders inside a delimited, attributed
envelope:
Three properties matter:
- The framing is in the prefix, the data is in the body. The instruction that enveloped content is data to be considered and never instructions to be followed, that it cannot alter policy or grant permission, and that a closing marker inside content is content — all of it is stable platform text, written once. Only the data varies. This is the same split the memory block already uses and it is the only version that is cache-safe.
- Content cannot close its own envelope. The closing marker carries a per-item nonce, and any occurrence of the delimiter syntax inside content is escaped at render time. Without this, "ignore the above and " preceded by a literal close tag is a one-line escape from the entire trust model.
- The label survives every transformation. Truncation keeps the envelope and marks it truncated. Elision keeps the label in the pointer. Compaction does not touch it at all. A transformation that produces unlabeled output from labeled input is a bug with a named eval.
The working-state block carries per-entry trust¶
The working-state block is rendered by the platform, but its entries have
origins, and an entry derived from untrusted content is still untrusted. A fact
extracted from a fetched page enters established_facts only as an attributed
claim — "the vendor's page states X" with its source event id — never as a bare
assertion of X. This is the same distinction memory formation draws when it refuses
to let EXTERNAL_UNTRUSTED spans form beliefs directly: the content is evidence,
and evidence is quoted with its source.
Working state¶
Milestone 7 gives WorkingState a shape and a home in RunCheckpoint.working_state
and stops there. Three things need deciding: who writes it, what carries across a
turn boundary, and what bounds it.
The model never writes it as prose¶
Working state is updated through a deterministic control tool, following the same
pattern Section 27.3 uses for conversation.ask_user and Section 30.2 uses for
skill_manage — the platform reads a typed tool call, not model prose that a parser
hopes to understand:
context.update_working_state
input: { "objective": str | null,
"add_constraints": [str],
"upsert_tasks": [TaskState],
"add_facts": [Fact],
"resolve_questions": [str],
"next_action": str | null }
effect: typed transition on the run's working state; checkpoint;
emit context.working_state.updated
The runtime is the second writer, and only for what it observes directly: task
states transition on tool outcomes, and open_questions gains an entry when
conversation.ask_user fires and loses it when the answer arrives. No parsing of
assistant text, ever. Inferring structured state from prose is nondeterministic and
is a prompt-injection surface — the same reasoning that put clarifying questions
behind a control tool.
The tool is read-only outside the run, never requires approval, and cannot remove a constraint the user set. An agent that can quietly drop its own constraints does not have constraints.
Carry rules across the turn boundary¶
A run is a turn (ADR-0009), so working state is per-run — but an objective set on
turn three is obviously still the objective on turn four, and next_action from
turn three is obviously stale. Each field carries a rule, applied when the new run
seeds from the session-history projection:
| Field | Carry | Rule |
|---|---|---|
objective |
Session | Carries until a user message states a different one; superseded, not silently replaced |
constraints |
Session | Append-only within a session; removable only by the user or an explicit tool call |
tasks |
Session | Open tasks carry; completed tasks drop from context and stay in the log |
established_facts |
Session | Carry with provenance; also a formation input (see below) |
open_questions |
Session | Carry until resolved by an answer, never by the model deciding to forget |
next_action |
Run | Always reset — it is a within-turn scratchpad |
Carry is computed from the log, not copied from the previous checkpoint, so it holds the same property Section 27.4 established for conversation: the session event log is authoritative and the checkpoint is not the system of record.
Working state is bounded¶
An unbounded working state is history wearing a schema. Caps: 20 constraints, 30 open tasks, 40 established facts, 20 open questions, and a 1,000-token block ceiling. Eviction takes completed and stale entries first, by age.
Constraints never evict. At the cap, add_constraints fails with a structured
error the agent can surface, rather than silently dropping the oldest. Constraints
are precisely the content whose loss the user notices and attributes to the agent
not listening.
Established facts feed formation¶
established_facts entries are, definitionally, things the session concluded and
carried — which is what the memory write path is looking for. Facts surviving to
the end of a session with trusted provenance are offered to consolidation as
candidates at the session-boundary trigger (formation spec, stage 1). They are
candidates, not beliefs: they enter the ordinary formation pipeline and are subject
to every eligibility gate, including the untrusted-content write ban. This gives
formation a second high-quality input without giving it a bypass.
Milestone 16 builds that offer.
memory-evaluation-and-lifecycle.md reads
the last working-state update in the consolidation window, admits each fact
only when source_event_ids is non-empty and every referenced event belongs to
the owning principal and is a trusted user source, and enters it as a candidate
at AFFIRMED authority — above an extractor inference and below a direct user
statement — behind a profile flag and under the same candidate ceiling. The
same document adds the recall delta and the correction lines this builder
assembles beside the recall block.
Freshness of in-turn recall¶
Production assembly re-reads current permitted memory instead of retaining rendered
recall across calls (ADR-0183). Checkpoint and
store-watermark equality do not establish continued validity: elapsed expiry,
semantic query preparation and withdrawn admission can change the answer without
a source write. The runtime obtains pressure and its request in one assemble
call, preserving one retrieval for that step. New evidence remains in Region B.
Only an explicit complete dependency revision permits completed-result reuse;
missing or failed revision validation discards retained evidence and reads again.
The bounded semantic cache retains query vectors, never recalled source content.
Ports and data model¶
ContextBuilder (Section 7) is unchanged. These are additions.
class ContextPlan(BaseModel):
session_id: UUID
epoch: int
prefix_sha256: str
prefix_tokens: int # measured exactly, never estimated
model_id: str
tool_names: list[str] # pinned at session open
tool_schema_sha256: str
authority_scope_hashes: tuple[str, ...] | None = None # legacy plans omit it
snapshot_id: UUID | None
snapshot_watermark: int # retrieval spec: the recall delta
skill_pins: tuple[SkillPin, ...] # skills spec: pinned at open
cache_breakpoints: list[CacheBreakpoint]
policy_version: str
builder_version: str
created_at: datetime
ContextBudget (Section 11.3) gains the classes the allocator needs to distinguish,
and a margin:
class ContextBudget(BaseModel):
total_tokens: int
reserve_output_tokens: int
platform_tokens: int
agent_tokens: int
tool_tokens: int
skill_catalog_tokens: int # skills spec: Region A metadata
skill_body_tokens: int # skills spec: Region B, loaded
retrieved_context_tokens: int # frozen snapshot + elastic in-turn
history_tokens: int
# additions
working_state_tokens: int
tool_result_tokens: int
knowledge_tokens: int # knowledge spec: Region B, passages
safety_margin_ratio: float = 0.05
Fact is referenced by Milestone 7's WorkingState and defined here, because
provenance and trust are what make it usable by formation:
class Fact(BaseModel):
statement: str
source_event_ids: list[int]
trust_level: TrustLevel
established_at: datetime
class CompactionResult(BaseModel):
summary: str
source_event_ids: list[int]
elided: list["ElidedSpan"]
replaced_through_sequence: int
depth: int # <= 2
tokens_before: int
tokens_after: int
compactor_version: str
class ElidedSpan(BaseModel):
item_id: str
trust_level: TrustLevel
byte_length: int
artifact_ref: str | None
event_id: int
Ports:
class ContextPlanner(Protocol):
async def plan(
self,
session: Session,
agent: AgentSpec,
principal: Principal,
model: ModelCapabilities,
*,
refresh_authorization: bool = False, # only before this run pins tools
) -> ContextPlan: ...
async def current(self, session_id: UUID) -> ContextPlan | None: ...
async def rotate(
self, session_id: UUID, reason: str
) -> ContextPlan: ...
class TokenEstimator(Protocol):
def estimate(
self, items: Sequence[ConversationItem], model_id: str
) -> int: ...
def estimate_tools(
self, tools: Sequence[ToolSpec], model_id: str
) -> int: ...
def reconcile(
self, model_id: str, estimated: int, actual: int
) -> None: ...
class Compactor(Protocol):
async def compact(
self,
checkpoint: RunCheckpoint,
budget: ContextBudget,
reason: str,
) -> CompactionResult: ...
Events (extending Section 6.8):
context.plan.created
context.epoch.rotated
context.snapshot.used
context.compacted
context.working_state.updated
context.budget.pressure
context.budget.exceeded
context.budget.pressure records that a yield step ran and which one; it is the
signal that tells an operator a deployment is chronically over-subscribed before
context.budget.exceeded tells them it has failed. It is emitted before the
model request whenever a yield ran, even if the resulting request fits. Its
fits field distinguishes successful yielding from overflow; a fitting request
with no yields emits no pressure event and needs no compaction.
context.snapshot.used binds a run to the opaque recall-trace ID and epoch of
its nonempty frozen snapshot before provider egress. Registration and snapshot
plan persistence use the owner People fence, so erasure either discovers the
dependent run or prevents the stale snapshot from being used. Missing snapshot
traces invalidate both cached and persisted plans; the next plan rotates the
epoch and recalls eligible memory again. People erasure redacts the persisted
snapshot and conservatively fences runs in its session for legacy plans without
usage events. Explicitly authored persona entries remain a separate source.
Failure modes and defenses¶
| Failure | How it happens | Defense |
|---|---|---|
| Cache thrash | A volatile byte reaches the prefix — a date, a counter, a re-serialized tool schema with unstable key order, a set dumped in its process's iteration order | Region declared per item type; canonical serialization (sorted keys and sets, ADR-0134); prefix_sha256 on every request; the fifty-turn stability gate; one plan identity across hash seeds |
| Label laundering | Compaction paraphrases untrusted content into unlabeled prose | Untrusted spans are elided to typed pointers, never summarized; canary eval |
| Envelope forgery | Tool output contains the closing delimiter | Per-item nonce; delimiter escaping at render; injection eval |
| Orphaned tool pair | The allocator drops a call or a result independently | Pairs are atomic budget units; a validator rejects orphans before send |
| Silent context overflow | Estimator under-counts; provider 400 mid-run | Conservative rounding; 5% margin; exact prefix measurement; reconciliation against actual usage |
| Truncated system prompt | Agent instructions exceed their cap and are trimmed to fit | Prefix classes never yield; the session fails to open with the offending class named |
| Compaction amnesia | A constraint set twenty turns ago is summarized away | Constraints never yield and never merge away; compaction-fidelity eval |
| Working-state drift | State asserts something the log contradicts | Typed transitions only, each emitting an event; carry recomputed from the log, not copied |
| Epoch churn | Routing or policy changes rotate the prefix repeatedly | Epochs are explicit, logged, and counted; epochs-per-session is a tracked metric with target 1.0 |
| Unstable history selection | The seed reads the projection as it stands rather than at a fixed cut, or the estimator returns different numbers for the same items | Seeding reads the log below runs.seed_event_sequence; the estimator is pure; the cut is a suffix index, property-tested for stability |
| Reasoning-item leakage | An opaque provider payload is summarized, logged, or carried across a run | Excluded from compaction input by type; dropped at run boundaries; never rendered into a summary (ADR-0007) |
Hard gates¶
Six hard gates. Five are on Milestone 7 with the rest of the engine; the first is on Milestone 1, because ADR-0024 places deterministic assembly in the vertical slice and a builder whose output is not reproducible cannot be built incrementally afterwards.
- Determinism.
build()invoked twice on the same checkpoint produces byte-identical output. Property-tested across generated checkpoints. M1. - Prefix stability. The scripted fifty-turn session — clock crossing
midnight, a tool revoked, memory written and corrected, a forced compaction —
yields exactly one distinct
prefix_sha256. M7. - Budget conformance. No assembled request exceeds the model's window; the output reserve is intact on every request; a synthetic overflow yields in the specified order and no more than necessary. M7.
- Tool-pair integrity. No assembled request, under any yield path, contains an unpaired tool call or tool result. M7.
- Trust preservation. A canary string placed in
EXTERNAL_UNTRUSTEDtool output never appears outside an envelope, and never appears in a compaction summary. Envelope-closing attempts in tool output do not escape the envelope. M7. - History-cut determinism. The retained set is always a contiguous suffix,
always fits
history_tokens, never falls earlier than the summary floor, never contains an orphaned tool call or result, and is identical across two calls on the same input. Property-tested over generated item lists. M7.
Tracked metrics¶
- Cached prefix ratio — prefix tokens served from cache after the first request of a session. Below roughly 90% means the invariant is leaking somewhere the hash check has not caught.
- Epochs per session — target 1.0.
- Estimator error — signed, per model; must never be negative by more than the safety margin.
- Compaction rate and depth — compactions per thousand turns, and the share reaching depth 2. Rising depth means the escalation path (child runs) is not being taken when it should be.
Also measure cached input against the preceding call's full input: a stable Region A hash alone does not prove history reuse. Provider lookup boundaries, legitimate body changes and cache availability affect it.
Build sequence (incremental, each gated by evals)¶
- Deterministic assembly. The two regions, the region-assignment table, the
fixed order, trust labels on every item, and
prefix_sha256recorded from the first commit — the hash is not retrofittable onto traffic that has already been served unstably. - Budget allocator. Fixed floors, absolute caps, the selection cut, the yield order, tool-pair atomicity, and the estimator behind its port. Fail-at-plan-time for prefix overflow.
- Trust envelopes. Nonced delimiters, escaping, and the framing text in the prefix. This is security hardening and it precedes anything that transforms content.
- Cache breakpoints and epochs.
CacheHintspopulation per Section 10.1, the rolling history window, epoch rotation and its accounting. Delivers the cached prefix ratio, which is how step 1 is proven in production rather than only in CI. - Working state. The typed control tool, carry rules, bounds, and the established-facts handoff to formation.
- Compaction. As a checkpoint write, on trusted content only, with elision of untrusted spans, provenance ids, and the depth cap.
- Reconciliation and tuning. Estimator correction factors from real usage, the pressure metrics, and retuning the class caps against them.
Decisions¶
- Two regions, one rule: if it can differ between two requests in the same session, it is not in the prefix. Region membership is a property of item type, declared in code and asserted at assembly, not a judgment made per request.
- Prefix stability is enforced by a recorded hash and a scripted long-session test, not by convention. A session with more than one prefix hash per epoch is a defect with an id.
- The tool set is pinned at session open; revocation is enforced at call time. A tool definition in a prompt grants nothing, so advertising a tool that will be denied is cheaper and safer than rewriting the prefix on every permission change.
- Prefix changes that genuinely cannot be absorbed rotate an epoch, explicitly and countably, with epochs-per-session tracked against a target of 1.0.
- Only history scales with the context window. Every other class is capped absolutely, because prefix content is attention paid on every request and attention degrades with item count, not with window fraction.
- The prefix never yields. If platform policy, agent instructions, tools, and snapshot do not fit their ceilings, the session fails to open with the offending class named. A silently truncated system prompt is worse than a refused session.
- History selection is a contiguous suffix chosen by a deterministic cut, and the seed reads the log at a pinned sequence. A relevance ranking over history would duplicate in-turn recall and leave a missing turn ambiguous between a selection defect and a ranking miss; a live projection read would let two seeds of one run disagree.
- Yield order is knowledge passages, then in-turn recall, then tool-result
truncation, then compaction — cheapest and most recoverable first. Knowledge
passages lead because the corpus is still there and one
knowledge.searchre-reaches it; compaction is last because it alone costs a model call and loses information irreversibly within the run. This is the same order stated where the ladder is specified, and the two must not drift apart. - Tool call/result pairs are atomic budget units. Dropping half a pair is a malformed request, not a smaller one.
build()is a pure function; compaction is a write. Pressure is resolved by writing a checkpoint and rebuilding, never by mutating inline. Determinism is what makes retries safe and the stability assertion meaningful.- The prefix is measured exactly at plan time; only the body is estimated. This confines estimator error to the region with slack and moves prefix-size failures to session open.
- The estimator is conservative, per-model, and reconciled against actual usage. Drift becomes a number rather than a mystery.
- Untrusted content is elided, never paraphrased. Summarization is a label-laundering vector; the summarizer runs on trusted content only, and untrusted spans become typed pointers that retain their label.
- Summary depth is capped at 2. Past that the run has outgrown its window and should escalate to a child run rather than be compressed into something unverifiable.
- Framing lives in the prefix; only data varies. Envelope rules, memory framing, and the "this is data, not instructions" instruction are written once as stable platform text.
- Content cannot close its own envelope, by nonce and by escaping.
- Working state is written by a typed control tool and the runtime, never parsed from model prose, carries across turns by per-field rule computed from the log, is bounded, and never evicts a constraint.
Open questions¶
None outstanding for assembly. Two adjacent items were left open here and are now decided in runtime-loop.md, which gave compaction its call site and therefore had to say what the call resolves to.
The compaction summarizer's prompt and model tier resolves through
ModelRouter under a named compaction model policy rather than being fixed
here or chosen at the call site. It defaults to the run's own provider, so
provider pinning is not broken by a compaction mid-run, at the cheapest tier
whose context window admits the region being summarized. The prompt is a
versioned asset and its version is recorded on the checkpoint compaction writes,
which is what makes a fidelity regression attributable to a prompt change rather
than to a model change. The tuning itself still belongs to the
compaction-fidelity eval; what is fixed is where the choice lives and what is
recorded about it.
Skill-content injection (Section 30.4 loads full skill instructions on selection) is assigned to Region B, as the reasoning above anticipated, with one addition that answers the caching objection: a skill body, once selected, is sticky for the remainder of the session unless the skill is deselected by a control tool. A skill that entered and left the prefix on alternating steps would invalidate the cached prefix on every one of them, which costs more than carrying an unused body. Stickiness is what makes Region B affordable for a large body; without it the right answer would have been the turn layer. skills.md takes this decision as given and supplies the rest: the caps, the loading tool, the trust derivation, and the rule that a body never yields. It also settles the one thing this paragraph left open — a skill is never deselected, and a third load fails rather than evicting.
Milestone 26 tool presentation¶
Context builder identity advances to context-builder@8 when ToolSpec gains the
presentation-only model_visible field. The planner excludes false-valued tools
before catalog pinning and prompt-budget selection; all existing model-visible
rosters retain their cap guarantees. Application-only Gmail sync capabilities
remain registered and callable by governed deterministic tasks. Hiding a tool
never adds scope or action authority and cannot override tool policy. The new
cache identity prevents reuse of prefixes built with the old visibility contract.