Documentation
¶
Overview ¶
Package contextengine assembles the message list sent to the model.
Everything here is a pure function of session state: no I/O, no clock, no randomness. That is not a style preference — it is what guards ADR-03. The context prefix must be byte-identical between turns or the provider's cache misses and every turn re-bills the full prompt.
Named contextengine rather than context so it never shadows the standard library package, which every file in this project imports.
Spec: docs/specs/architecture/context-engine/202608072333-*.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultBands = []float64{0.60, 0.80, 0.92}
DefaultBands are the announcement thresholds, ascending, as fractions of the BUDGET rather than of the window.
The distinction is forced, and it is the one correction this change makes to its own spec. The spec declares the bands as fractions of the window — 0.60, 0.80, 0.92 — and separately requires every band to sit below CompactAt, which defaults to 0.80. Both cannot hold: two of the three bands are unreachable, because the context is cut at 0.80 and never gets to 0.92.
Read as fractions of the budget, both statements hold and the meaning improves. The budget is the space before compaction, so "you are at 92%" says 92% of what you get before your memory is cut — which is exactly the thing the model can act on. Against the window it would have been a number about a limit that never arrives.
var ErrNoInstructions = errors.New("contextengine: session has no instructions")
ErrNoInstructions is returned when a session carries no system prompt. An agent with no doctrine is not a degraded agent, it is an unpredictable one.
Functions ¶
func Estimate ¶
Estimate approximates the token count of msgs.
Deliberately a character heuristic and not a real tokenizer: the trigger is a fraction of the window with a safety margin, so precision is not needed. What is needed is determinism — an estimate that drifts between runs makes the compaction golden tests flap.
func Fraction ¶
Fraction is how much of the window the assembled context occupies, in [0,1].
The number already existed and was already computed on every iteration of the loop — Plan compares exactly this against the compaction trigger. What it was not, was reachable by anyone who could act on it. This exposes it.
Pure, like Estimate, and the same arithmetic Plan performs.
Types ¶
type Band ¶
type Band int
Band is how full the context was the last time the model was told.
It lives in session state rather than being derived per turn because the announcement is edge-triggered: emitting while the fraction is merely ABOVE a threshold repeats the same reminder every turn, which costs tokens and, worse, produces habituation. A warning that is always there stops being read.
func BandFor ¶
BandFor returns the band a window fraction falls in. Pure.
compactAt is a parameter rather than a package constant because the trigger is configuration: a session that compacts at 0.5 has a smaller budget, and the bands have to move with it or they stop meaning anything.
type CompactionPlan ¶
CompactionPlan describes the span to replace with a single summary. FromIdx is inclusive, ToIdx exclusive, both indices into Session.History.
func Plan ¶
func Plan(s Session, cfg Config) (CompactionPlan, bool)
Plan decides whether to compact and where to cut. Pure: it only decides, and the caller generates the summary text, because that needs a model call and would drag I/O into this package.
Two cuts are never made:
- inside a turn, splitting an assistant message from its tool results — that produces history no provider will accept;
- at or after the most recent user message — the current task always survives by construction, not by summary quality.
type Config ¶
type Config struct {
// CompactAt is the fraction of the model window that triggers compaction.
CompactAt float64
// KeepTurns is how many recent turns survive beyond the mandatory ones.
KeepTurns int
// KeepFraction is how much of the window the tail must keep, as a fraction.
//
// It exists because KeepTurns alone is a count, and turns vary by an order
// of magnitude: four short ones protect almost nothing, and four long ones
// leave almost nothing to compact. The two are read together and whichever
// protects MORE wins — the count is a floor for how many exchanges survive,
// the fraction a floor for how much of them does.
KeepFraction float64
// CharsPerToken is the estimation heuristic.
CharsPerToken float64
// Margin is added to every estimate to absorb heuristic error.
Margin float64
// Window is the model's context window in tokens. Supplied by the provider.
Window int
}
Config carries the knobs Plan and Estimate need. Passed in rather than read from the environment, so both stay pure.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig mirrors the defaults documented in the config spec.
type Image ¶
type Image struct {
// MediaType is the wire type — image/png, image/jpeg, image/gif,
// image/webp. Named rather than sniffed at encode time so the one place
// that decides it is the one that read the file.
MediaType string
Data []byte
}
Image is a picture the model is shown.
Carried as bytes rather than as a path: Assemble is pure and its output must be byte-identical for the same input, and a path is a thing that varies by machine and can change under the session (RN-7).
type Message ¶
type Message struct {
Role Role `json:"role"`
Text string `json:"text,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolResult *ToolResult `json:"tool_result,omitempty"`
// Images ride with the text rather than in a field of their own further
// up, because a message is one thing the model reads and splitting it
// loses which picture went with which question.
Images []Image
// Reminder marks a message the harness appended rather than the user
// typing it. It rides on the user role because that is the only channel
// every provider accepts mid-conversation, but a client must never render
// it as something the user said.
Reminder bool `json:"reminder,omitempty"`
}
Message is one entry of the model conversation. It is the neutral type: no provider-specific shape reaches beyond the provider package.
func Assemble ¶
Assemble builds the message list for one model call.
The order is fixed, most stable first, so the provider can match the longest possible cached prefix:
- system prompt — never changes within a session
- tool defs — frozen at creation
- summary — changes only on compaction
- live history — append-only
Blocks 1 to 3 collapse into a single system message: they are one immutable unit, and splitting them would let a provider that keys the cache on message boundaries miss on a summary change alone.
Assemble is pure. Calling it twice with the same Session yields byte-identical output, which is what makes golden testing exact.
type Session ¶
type Session struct {
// Instructions is the already-built system prompt. The behavior package
// composes it; this package only places it.
Instructions string
// Tools is frozen at session creation.
Tools []ToolDef
// Summary is nil until the first compaction.
Summary *Summary
// History is append-only. Nothing already in it is ever edited.
History []Message
}
Session is the complete state Assemble is a function of. Nothing else is read: no globals, no environment, no clock.
type ToolCall ¶
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
}
ToolCall is a model request to run a tool.