contextengine

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 3 Imported by: 0

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

View Source
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.

View Source
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

func Estimate(msgs []Message, cfg Config) int

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

func Fraction(s Session, cfg Config) float64

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.

const (
	BandNone Band = iota // nothing announced yet
	Band60
	Band80
	Band92
)

func BandFor

func BandFor(f, compactAt float64) Band

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.

func Crossed

func Crossed(announced Band, f, compactAt float64) (Band, bool)

Crossed reports the band to announce, and whether to announce at all.

Upward only. Going back down — which is what compaction does — announces nothing, but it does rearm: the returned band becomes the new high-water mark, so climbing back up is news again, and genuinely is.

type CompactionPlan

type CompactionPlan struct {
	FromIdx int
	ToIdx   int
}

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

func Assemble(s Session) ([]Message, error)

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:

  1. system prompt — never changes within a session
  2. tool defs — frozen at creation
  3. summary — changes only on compaction
  4. 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 Role

type Role string

Role identifies who produced a message.

const (
	RoleSystem    Role = "system"
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleTool      Role = "tool"
)

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.

func Apply

func Apply(s Session, plan CompactionPlan, text string) Session

Apply returns the session with plan applied and text as the new summary. The original History slice is not mutated: append-only means the caller's view stays valid.

type Summary

type Summary struct {
	Text    string `json:"text"`
	UpToIdx int    `json:"up_to_idx"`
}

Summary replaces a compacted span of history. UpToIdx is exclusive.

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.

type ToolDef

type ToolDef struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	Schema      json.RawMessage `json:"schema"`
}

ToolDef is a tool declaration. The set is frozen at session creation: a definition that appears mid-session invalidates the whole cached prefix.

type ToolResult

type ToolResult struct {
	ToolCallID string `json:"tool_call_id"`
	Output     string `json:"output"`
	IsError    bool   `json:"is_error"`
	Truncated  bool   `json:"truncated"`
}

ToolResult is the outcome fed back to the model. IsError is not a failure of the turn: the model reads it and recovers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL