ultrarouter docs
Guides

The context runtime

Deterministic context assembly — lift, compile, render and a pure exit — plus compatibility profiles, memory classes, the lazy SOP catalog and native-delivery adapters. Adopt in one file, leave in one file.

The context runtime is a pure library that decides, deterministically, what one model call is allowed to see — and records that decision as a hashable manifest. It is @ultrarouter/context, shipped in the MIT-licensed SDK as @ultrarouter/sdk/context (TypeScript) and ultrarouter.context (Python). It reads no clock, no environment, no network and no random source: the same inputs always produce the same manifest hash.

Two promises make it safe to adopt:

  • The gateway is a witness, never an author. When a request carries a manifest (ur.manifest), the gateway verifies every declared hash against the bytes it received and stores what it saw; it never adds, drops or reorders a module. The single documented exception is the Flywheel's learned data slot, described at the end of this page.
  • The exit is a pure function. toMessages(fromMessages(m)) returns a message array byte-identical to m. Nothing in the gateway requires a manifest, so you can adopt the runtime in one file and leave in one file.

Availability: the runtime and the guarantees on this page ship today in the SDK context subpath. The gateway's ur.manifest witness and the dashboard surfaces are part of a planned build and are documented separately as they ship. This page documents the pure library you can use now — it does not describe a hosted prompt service (there is none).

Lift → compile → render → exit

The runtime has four pure entry points. You can use any of them alone.

Lift — label an existing prompt

fromMessages runs the same segmenter the gateway uses over a plain OpenAI messages array and returns a labelled manifest plus the segmenter's own classification. No model call, no mutation.

import { fromMessages, toMessages } from '@ultrarouter/sdk/context';

const messages = [
  { role: 'system', content: 'You are a careful refund assistant.' },
  { role: 'user', content: 'Draft a refund reply for order 91237.' },
];

const { manifest, labels } = fromMessages(messages);
// `labels` are the gateway segmenter's kinds (system_instructions, user_question, …)
// and eligibility — the SAME code path the hot path runs, never a copy.

const restored = toMessages(manifest);
// `restored` is byte-identical to `messages`: leaving is a pure function (CTX-50).
from ultrarouter.context import from_messages, to_messages

messages = [
    {"role": "system", "content": "You are a careful refund assistant."},
    {"role": "user", "content": "Draft a refund reply for order 91237."},
]

lifted = from_messages(messages)
restored = to_messages(lifted["manifest"])
assert restored == messages  # the exit is pure — adopt in one file, leave in one file

Compile — assemble from modules under a contract

compile(envelope, contract, candidates, profile) runs the normative admission algorithm and returns { ok, manifest, refusals }. Nothing is truncated: a module either fits the budget whole or is shed with a recorded reason.

import { compile, ULTRAROUTER_PROFILE } from '@ultrarouter/sdk/context';

const envelope = {
  id: 'exec-1',
  tenant: 'acme',
  stage: 'assistant',
  mode: 'live',
  asOf: '2026-04-01T00:00:00.000Z',
  cutoffs: {},
  budgets: { tokens: 25 },
  seed: 'seed-1',
  pins: [],
  releases: {},
  permissionEpoch: 'epoch-1',
};

const contract = {
  stage: 'assistant',
  requiredKinds: [],
  optionalKinds: ['procedure', 'fact', 'history'],
  forbiddenKinds: [],
  budgets: { tokens: 25 },
  limits: [],
  pinPolicy: 'none',
  slots: ['instruction', 'reference', 'exemplar'],
};

const module = (id, kind, memoryClass, trustClass) => ({
  id,
  version: '1',
  kind,
  memoryClass,
  trustClass,
  privacyClass: 'workspace_private',
  availability: { from: null, to: null },
  contentHash: `sha256:${id}`,
  tokenEstimate: 10,
  facets: {},
});

const candidates = [
  { module: module('runbook', 'procedure', 'procedural', 'approved_derived') },
  { module: module('pricing', 'fact', 'semantic', 'authoritative') },
  { module: module('thread', 'history', 'episodic', 'approved_derived') },
];

const { ok, manifest, refusals } = compile(envelope, contract, candidates, ULTRAROUTER_PROFILE);
// With a 25-token budget the two most stable modules are admitted (procedural, then
// semantic); the episodic `thread` is shed with a BUDGET_EXCEEDED refusal on the manifest.

The Python surface is the same call with snake_case helpers and camelCase dict keys:

from ultrarouter.context import compile, ULTRAROUTER_PROFILE

result = compile(envelope, contract, candidates, ULTRAROUTER_PROFILE)
manifest = result.manifest        # None when result.ok is False
refusals = result.refusals        # every shed / inadmissible module, with its reason

Render — turn a manifest into provider messages

render(manifest, bodies, serializer?) maps each admitted entry's moduleId to its body text and emits provider messages. The default serializer concatenates instruction-slot modules into one system message and fences reference/exemplar modules behind a safety note so untrusted data can never be read as an instruction.

import { render } from '@ultrarouter/sdk/context';

const bodies = new Map([['runbook', 'Verify the order before offering a refund.']]);
const providerMessages = render(manifest, bodies);

Compatibility profiles

compile is parameterised by a profile so one compiler serves every adopter. A profile carries the ordering rule, the memory-class and refusal vocabularies, the profile-level limits, the render serializer and the manifest encoder (its serialization and hash subject).

  • ULTRAROUTER_PROFILE is the default for new adopters. It orders admitted modules by volatility — stable material first (procedural), volatile last (episodic) — which is prompt-cache friendly. Ordering is part of manifestHash, so switching an existing workload's order rule is a model-visible change and goes through the ratchet like any other lever; it is never silent.
  • Parent profiles (the two sibling engines this runtime was extracted from) declare their own selection order, memory classes, refusal codes and encoder. Under a parent's profile its fixture corpus compiles byte-identically — same manifest bytes, same refusal codes, same rendered messages. That equality is the parity guarantee; a parent's private fields ride on an opaque ext that the compiler round-trips but never reads for authority.

Memory classes

Every module declares a memoryClass. The core four are a closed vocabulary; a profile can extend them with defineMemoryClasses(extra). The compiler treats each class as an opaque label — it never branches on it — but the default volatility order and shed priority read it, so the class you assign decides what is kept when the budget tightens.

ClassMeaningVolatility (default order)
proceduralhow to act — rules, runbooks, SOPsmost stable — kept longest
semanticdurable facts and definitionsstable
workingthis turn's scratch statevolatile
episodicconversation and event historymost volatile — shed first

Budgets and refusals

Admission sheds whole modules, never truncates. Mandatory pins are admitted first and are never shed; optional modules are admitted by keep-priority until the token (or count) budget is reached, and each module that does not fit is recorded as a BUDGET_EXCEEDED refusal on the manifest rather than being silently dropped.

Refusals are data, not exceptions, so one manifest carries both what it admitted and what it refused — the ledger the witness and the Flywheel read. The base vocabulary is closed and includes STAGE_CONTRACT_MISMATCH, REQUIRED_CONTEXT_MISSING, FORBIDDEN_KIND, AVAILABILITY_REFUSED, LIMIT_EXCEEDED, BUDGET_EXCEEDED and UNTRUSTED_IN_INSTRUCTION_SLOT. The last one is a fail-closed rule: a module whose trustClass is untrusted may only land in a reference or exemplar slot; an explicit attempt to place it in an instruction slot is a refusal, and the gateway rejects the same placement as an invalid_request.

The SOP catalog: compact when planning, full when executing

A Standard Operating Procedure is authored as prose but prose never executes. A reviewed SOP compiles to an immutable ExecutableSopSpec — ordered phases with allowed actions, required policies, budgets, stop conditions and artifact contracts — bound to the exact reviewed bytes (SOP_REVIEW_STALE if the prose changed after review). Branching is data: a typed transition language (artifact_present(contract), stop_condition(id), budget_remaining(kind) > n, and the all(…) / any(…) combinators) whose graph is checked acyclic at compile time, separately from the dependency graph.

The catalog is what makes "dozens of SOPs that load themselves in branches" affordable: each SOP publishes a descriptor of ≤ 240 characters (≈ 60 tokens under the repo's chars/4 estimator) — its id, its applicability when, its phase names and what it produces, but not its body. The model plans against descriptors only; a full body is loaded lazily, at the gated phase boundary, through a SopLoadPort the planning plane never calls.

import { compileSop, catalog, eligible } from '@ultrarouter/sdk/context';

const result = compileSop({ draft, review }); // { ok, spec } | { ok: false, refusals }
if (result.ok) {
  const plan = catalog([result.spec]);       // ~60-token descriptors, no bodies loaded
  const admissible = eligible([result.spec], runtimeState); // ids whose `when` predicate holds
}

The descriptor budget is an input contract, not a hope: an over-budget descriptor is a DESCRIPTOR_OVER_BUDGET compile refusal that names which limit blew, never a truncation. A 30-SOP catalog stays under ~2,000 planning tokens — the two or three bodies a run actually reaches are loaded on demand, instead of pasting every procedure into every prompt.

Single use is a property of the store, not a claim of a pure function: authorizePhase builds an execution-bound receipt {spec_hash, phase, args_hash, exec_id} and asks an AuthorizationReceiptPort to record it; a reused receipt is refused because the store says duplicate. Two fallbacks are themselves manifest entries so they stay visible in the ledger: mandatoryOnlyManifest (drop to the phases that must always run when the planner bound is exceeded) and gapArtifact (a named "what this run could not observe" value a gate can read).

The Python subset ships lift, compile, render, exit, profiles and memory classes today; the Python SOP compiler is a filed follow-up. Use the TypeScript subpath for SOP compilation until it lands.

Native-delivery adapters

When a provider ships its own context features, the runtime can deliver a compiled manifest through them instead of pasting everything inline — the runtime decides what deserves the window; the provider decides how the window is managed. planNativeDelivery takes a compiled manifest and a target and decides, entry by entry, which channel carries it. The decision is a table lookup: no branch names a provider.

v1 supports one provider natively — the Anthropic Messages API — with a frozen mapping:

Manifest entryNative channelProvider feature token
SOP / tool descriptorsdeferred tool (tool search)tool_search_tool_regex_20251119
episodic modulesserver compaction windowcompact-2026-01-12
episodic modulescontext-editing windowcontext-management-2025-06-27
procedural modules (app opted in)memory toolmemory_20250818
turn-scoped operator textmid-conversation system messagemodel-gated (no beta header)

Two rules keep it honest. Absence is the fallback: a (provider, model, feature) triple with no row in the compatibility table — every non-Anthropic provider, and any Anthropic request that does not carry the required beta header or opt-in — takes the ONE documented fallback: the catalog is rendered inline and the manifest records native_delivery: 'unavailable'. Model gating is data: claude-haiku-4-5 has no memory tool or mid-conversation-system row, so those entries fall back on that model — a missing row, not a special case. A refusal (NATIVE_UNAVAILABLE) is emitted only when the contract required native delivery; a contract that merely prefers it degrades silently but recorded. Native and inline delivery carry the same set of facts — same module ids, hashes and classifications — only the channel differs.

Adopt in one file, leave in one file

Because the exit is pure and nothing on the hot path needs a manifest, adopting the runtime is reversible by construction:

  • Adopt: import from @ultrarouter/sdk/context (or ultrarouter.context), lift or compile your prompt, and send render(...) output to any OpenAI-compatible endpoint. ur context lint (an advisory scanner that never modifies a file) flags oversized inline prompts and untrusted values interpolated into instruction surfaces.
  • Leave: call toMessages(manifest) and you are back to a plain messages array, byte-for-byte. A stored manifest plus its bodies round-trips as plain JSON. There is no service to turn off.

The learned slot (Flywheel)

The gateway's zero-authority rule has exactly one exception: the Flywheel's learned data slot. A consented, holdout-tested learned module — entered as memoryClass: semantic, trustClass: approved_derived, behind a standing holdout — is the one thing the runtime may inject on your behalf, and only after a live confirmation has proven it. Nothing else the gateway sees is ever added to your context. See The Flywheel for the loop and Holdout and lift for how an improvement is proven before it can teach.

Next steps

  • The five products — where Context sits among Router, Evals, Scoring and Flywheel.
  • For agents — the machine-first surfaces and the product-select blocks.
  • Record outcomes — feed the ledger the learned slot is built on.