A map, not a knowledge dump.
We embed an LLM agent in our product — a work-management platform: projects, work items, reported data. A user opens the assistant inside a project and asks: “which items are at risk before their deadline?” Answering that takes knowledge no foundation model has: what a work item is in this product, that deadlines and assignees live on it, which workflow states exist, and that display names come from configuration — so echoing a raw id back at the user is a bug, not an answer.
The obvious move is to paste all of that into the system prompt. Everybody’s
first harness does this, ours included. It fails slowly, then suddenly: the
system prompt is re-rendered on every iteration of every turn — in our
harness literally so, buildSystemPrompt runs once per LLM call — so every
character of standing context is a tax you pay for the lifetime of the
conversation. Domain knowledge only grows. Paste-everything doesn’t scale, and
the usual escape hatch — truncating history when things get tight — throws away
exactly the context an agent needs most.
This series is about the third option, the one our harness commits to everywhere: don’t truncate, defer. Keep an index in the standing context and make the payload something the model pulls on demand — pointers, not payloads. 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: the more that’s in front of a model, the worse it uses any one piece of it, and irrelevant schemas and reference text don’t just cost tokens — they compete with the task. What makes deferral work is that it is not omission: everything stays reachable by a stable address, so shrinking the standing context removes noise from the turn without removing information from the system. Less context is more accuracy, exactly as long as nothing necessary becomes unreachable — and keeping the index rich enough that the model knows what to pull is what the mechanisms in this series are careful about.
Everything here is from the production harness in our backend — the mechanisms, the code, and the numbers, which are measured from its source using the rough conversion of one token per four characters.
A graph, not a document
The ontology is not prose. It’s a typed graph: 15 domains (bounded
contexts like work-items, statuses, data-points), 28 entities, each
owned by exactly one domain, and 12 relationships with ids derived as
<from>.<predicate>.<to>:
export const WORK_ITEM_ASSIGNED_TO_MEMBER = new Relationship(
WORK_ITEM,
"assigned-to",
MEMBER,
"A work item is assigned to a member responsible for it."
);
Two design rules do a lot of quiet work here.
First, references into the graph are validated, not stringly-typed.
Anything that tags itself with a concept — a tool declaring
semantics = [WORK_ITEM], a memory scoped to an entity — is checked against
the ontology, so a bad id fails at authoring time instead of silently matching
nothing at runtime. The dependency is strictly one-way: tools point into the
ontology; the ontology has never heard of a tool. And a tool’s domain
membership is derived from its tags, never authored, so it can’t drift out
of sync.
Second, knowledge lives once. Entity notes — the operational gotchas: the
words users actually say instead of the entity’s official name, the lookups to
perform before presenting something, the edits that silently fail — are data on
the entity, rendered wherever the entity is rendered. The interface docs are
explicit that notes are “never hand-written into prompt strings”. There is
exactly one place to fix a wrong fact.
What actually ships every turn
Here is the entire standing footprint of the ontology, from Ontology.toPrompt():
public toPrompt(): string {
const roots = this.domainList
.filter((d) => d.getParent() === null);
if (roots.length === 0) return "";
const lines: string[] = ["Domains in this scope:"];
for (const domain of roots)
lines.push(`- ${domain.id} — ${domain.description}`);
return lines.join("\n");
}
Root domains only, one line each. The in-code comment states the intent better
than I could: deliberately brief — a standing orientation (valid ids and where
things live), not a knowledge dump. Before rendering, forScope filters
the graph down to what the current scope can even mean: domains that can’t
apply where the agent is standing are dropped wholesale, and whatever the user
is currently looking at — the scope references passed to the agent — pulls its
owning domains in. A typical scope leaves 13 lines:
A five-and-a-half-times reduction is nice, but it’s the smallest number in this series. The point of starting here is that the ontology establishes the pattern — and, as we’ll see, the coordinate system — that the bigger savings in parts 2 and 3 are built on.
Depth is a tool call
The model reaches everything below the root index through one always-available
tool, explore_ontology, with three modes:
Calling it with no arguments returns the full platform domain map — the
escape hatch for tasks that reach beyond the current scope. Erroring on the
empty call was explicitly rejected in the source: it “would just force a
guess.” Passing ids drills in: a domain returns its entities and
relationships plus the neighbouring domains reachable through them; an entity
returns its description, its notes, and every relationship touching it. Passing
a query runs BM25 keyword search over ids and descriptions, so “deadline”
finds work-item through its description without the model knowing the id.
One asymmetry is worth pausing on. explore_ontology has no pagination, no
character caps, no limits at all — while search_tools, its sibling in the
next post, is budgeted to the character. That’s deliberate. The ontology corpus
is small, static and read-only; concept text can’t flood anything that matters.
Tool results, as we’ll see, can. Budgets are applied where the failure mode
lives, not uniformly.
The index is also a coordinate system
If this were only about shrinking a prompt section, an ontology would be overkill — a hand-tuned paragraph could compete. The reason it’s a graph with stable ids is that those ids become the shared vocabulary for everything else in the harness:
search_toolsaccepts the samedomains/entities/relationshipsids as filters, so the model can ask for “tools touchingwork-item” — and a tool tagged with a relationship is findable from either endpoint entity.- Each entity’s glossary text is folded into the search document of every tool that touches it, giving lexical search a curated synonym layer with zero embeddings involved.
- Memories are tagged with the same concepts, scoping recall.
- Even the value references of part 3 participate: every scope reference knows
getEntity()andgetDomain(), which is exactly whatforScopeconsumes.
That’s the red thread of this series: the ontology isn’t a prompt section, it’s the address space the other two mechanisms route through.
What it costs, honestly
Depth costs a decision. Every deferred layer is a round trip the model has to choose to make. A model that never drills is operating on 13 one-liners, so those lines have to be written as signals — descriptive enough that the model can tell something worth pulling sits underneath. Deferral moves the hard work from budgeting tokens to writing an index that provokes the right pulls.
The map must track the territory. An ontology is a second description of the product, and second descriptions drift. The standing section is the layer the model trusts most — it arrives as system prompt, not as a tool result — so a stale description doesn’t just miss, it misleads every conversation. Curation is a permanent chore: the index stays small and true because someone keeps it that way.
The ontology solves “what does the domain mean” for a few hundred standing tokens. The next problem is an order of magnitude larger: our agent has 169 tools, and their JSON schemas alone are ~85,000 characters. Declaring them all would cost more than every other prompt section combined — so the harness doesn’t, and the machinery it uses instead is the subject of part 2.