Context Engineering in an LLM Harness — Part 1: Ontology — Henrik Udnes Henrik Udnes Systems architect & software engineer
Imagine embedding an LLM agent in an issue tracker. A user opens the assistant<br>inside a release project and asks: “which open issues are blocking the<br>release?” The model needs product-specific knowledge: how projects, issues,<br>workflows and dependencies fit together; which status names this team uses;<br>and how internal ids map to the names people see. Echoing a raw id back at the<br>user is still a bug, not an answer.
The obvious move is to paste all of that into the system prompt. Everybody’s<br>first harness does this, ours included. It works, but its cost grows with the<br>domain: the system prompt is re-rendered on every iteration of every turn —<br>in our harness literally so, buildSystemPrompt runs once per LLM call — so<br>every character of standing context is a tax you pay for the lifetime of the<br>conversation. Domain knowledge only grows. At sufficient scale,<br>paste-everything becomes expensive, and the usual escape hatch — destructively<br>truncating history when things get tight — throws away exactly the context an<br>agent needs most.
This series is about the third option, the one our harness commits to<br>everywhere: defer, don’t discard . Keep an index in the standing context<br>and make the payload something the model pulls on demand. When even a pulled<br>result is too large, bound the view and keep the exact value behind its pointer.<br>Applied five times, at five different altitudes:
Ontology (this post) — what the domain is
Tool discovery — what the agent can do
Values by reference — the data flowing through it
Memory — what the agent remembers
Skills — what users teach it
Cost is only half the argument. A context window is also an attention budget:<br>the more that’s in front of a model, the worse it uses any one piece of it, and<br>irrelevant schemas and reference text don’t just cost tokens — they compete<br>with the task. What makes deferral work is that it is not omission:<br>everything stays reachable by a stable address, so shrinking the standing<br>context removes noise from the turn without removing information from the<br>system. Less context is more accuracy, exactly as long as nothing necessary<br>becomes unreachable — and keeping the index rich enough that the model knows<br>what to pull is what the mechanisms in this series are careful about.
The mechanisms and measurements here come from a production harness; the<br>running examples use a generic issue tracker so the architecture stays legible<br>without product knowledge. Token estimates use the rough conversion of one<br>token per four characters.
IN CONTEXT, EVERY TURN — THE INDEX ON DEMAND — THE PAYLOAD Domain ontology 13 root-domain one-liners · ≈340 tokens Concept graph 15 domains · 28 entities · 12 relationships explore_ontology Tool surface namespace line + scaffolding + learned/active set Tool catalog 169 tools · ≈85k chars of schema search_toolsactivate_tool Skill catalog id — title: description per skill Skill bodies full instructions while active activate_skilldeactivate_skill Memory digest capped index of {key, description} Memory bank capped records, key-addressed recall_memories Tool results bounded previews + $ref stubs Blob store unbounded bytes, server-side $ref resolution The shape of the whole series: the standing context holds five small indexes; every payload lives behind a pull. This post is the first row.<br>A graph, not a document
The ontology is not prose. It’s a typed graph: 15 domains (bounded<br>contexts such as issues, workflows and labels), 28 entities , each<br>owned by exactly one domain, and 12 relationships with ids derived as<br>..:
export const ISSUE_ASSIGNED_TO_USER = new Relationship(<br>ISSUE,<br>"assigned-to",<br>USER,<br>"An issue is assigned to the user responsible for it."<br>);<br>projects issues labels workflows users attachments belongs-to labeled-with assigned-to defined-by contains project issue label status workflow user attachment file status · note (drill-in only) “Users may call statuses ‘stages’ or ‘columns’.” The production graph pattern mapped onto an issue tracker. Entities live inside their owning domain; relationships may span domains. Notes carry operational knowledge that ships only when something drills in.<br>Two design rules do a lot of quiet work here.
First, references into the graph are validated, not stringly-typed .<br>Anything that tags itself with a concept — a tool declaring<br>semantics = [ISSUE], a memory scoped to an entity — is checked against<br>the ontology, so a bad id fails at authoring time instead of silently matching<br>nothing at runtime. The dependency is strictly one-way: tools point into the<br>ontology; the ontology has never heard of a tool. And a tool’s domain<br>membership is derived from its tags, never authored, so it can’t drift out<br>of sync.
Second, knowledge lives once . Entity notes — the operational gotchas: the<br>words users actually say instead of the entity’s official name, the lookups to<br>perform before...