I built a system to tell me what to wear. It is smaller than the problems I usually work on. Most of my career has been data infrastructure inside regulated environments. I chose a wardrobe because I could personally check every answer. If a system tells a satellite operator something wrong, it may take a while to find out. If it tells me to wear suede loafers while it is raining, I find out in about eleven seconds.
The architecture looked familiar: an orchestrator over three specialists, one each for weather, wardrobe, and outfit assembly. MCP reached the data. A2A reached the agents. An Agent Card described how to call each one. Google's ADK held the pieces together.
The system worked well enough to expose a deeper problem.
The failure
It returned an outfit with fluent reasoning attached. Then I looked out the window.
The suede loafers had passed every check the system knew how to run. They had the right formality, matched the palette, were clean, were in season, and were in the closet. Six columns described each garment, and the shoes were valid on all six.
None of those columns could express what rain does to suede.
The model had not malfunctioned. It reasoned over the world I gave it, and that world omitted the fact that mattered. The system could not represent “this material degrades under this condition,” so it filled the gap with a plausible answer.
Why the first fixes failed
I started with the prompt. I told the system about rain. It improved on that case and failed on the next one: linen in forty-degree weather, white sneakers on a construction site, a wool coat in Atlanta in August.
Then I upgraded the model. A stronger model guesses better because it has absorbed more of the world. That makes the failure rarer, not impossible. Rare failures are harder to reproduce, and the user usually finds them before the engineer does.
I added a materials agent and a context agent. The diagram got more impressive. The reasoning did not. Each new agent received the same incomplete description and improvised over it. I had added more places to guess and given them a protocol for exchanging the guesses.
Better agents did not give me better answers. I had treated the problem as transport: how do these services talk? The failure was representation: what does the system know, what do those facts mean, and what may it conclude from them? MCP and A2A handle transport and discovery. They do not make the payload true.
The substrate can be wrong
Frank Coyle's talk “Why Agentic Systems Need Ontologies” helped reorganize my thinking. We often call an incorrect model output a hallucination when part of the failure belongs to the world we handed the model.
A reasoner working over an incomplete domain still produces an answer. If a shoe is six columns and none record material behavior under weather, “wear the loafers” is valid against the data. It is invalid against the world. Fixing the representation addresses that gap without waiting for a new model release.
A rulebook for the domain
Tom Gruber's 1993 paper defines an ontology as “an explicit specification of a conceptualization.” I use a plainer description with teams: an ontology is a rulebook for your data.
A schema says a column contains a string. The rulebook defines what that string means, what it can coexist with, where it came from, and what else the system may infer.
Your closet is already an ontology; you have not written it down. You know suede and rain do not mix. A blazer means something different over a T-shirt than over a button-down. The shirt you wore yesterday is unavailable today, which is a fact with a timestamp. “Business casual” in Atlanta in August is not the same garment set as “business casual” in Zurich in March.
Writing that knowledge down required five parts.
- Vocabulary
- A closed set of defined terms. If
formalityaccepts five values, a sixth is an error instead of a novel interpretation. - Semantics
- The relationships between terms, so the system can separate different wording from different meaning.
- Constraints
- Facts that cannot be true together, such as suede with precipitation or linen below a chosen temperature.
- Provenance
- Where a fact came from and when. A weather reading from four hours ago is not the same object as one from four minutes ago.
- Inference contract
- The conclusions the system may derive and the rule required to license each one.
The evidence graph
In the rebuilt system, one blazer carries nine relationships. Five were asserted directly; four were derived. Every derived edge names the rule that produced it.
Naming the rule changes the design. When the system says “do not wear this,” I can ask why. The answer is not generated prose shaped like reasoning. It is a rule identifier and the facts that triggered it. A conclusion with no rule is a guess wearing a conclusion's clothes.
The rebuild added two other controls that mattered more than I expected.
Bounded staleness
Projections have a maximum age of 120 seconds in this implementation. After that, the data is absent rather than stale-but-usable. The system cannot quietly reason over an old fact.
Refusal
The system declines to answer when data is missing, constraints conflict, or an inference has no rule. That choice makes a weaker demo and a better product. An honest refusal is a bug report. A confident wrong answer becomes a user report later.
The model does less at every version
V1 asked the model to interpret the request, recall the domain, apply the rules, and produce an answer. V2 moved the rules into code and left interpretation and assembly with the model. V3 moved the domain into an explicit contract. The model now handles language: understanding the request and phrasing the result. The contract handles truth.
One commit removed three to five model round trips by replacing them with contract evaluation. Typical latency fell from roughly twenty to forty seconds to about six. Correctness improved, model cost fell, and explanations came from rules instead of post-hoc rationalization.
Language models are good at language. We keep assigning them the jobs of a database, rule engine, and domain expert, then debugging the seams one prompt at a time.
When the contract earns its cost
I do not put a formal ontology under every application. I build one when wrong answers are expensive, when the domain contains constraints the data does not express, when somebody will need an explanation later, or when multiple agents must agree on the meaning of a term.
The multi-agent case is especially direct. Once two agents exchange a message, they rely on a shared meaning neither protocol defines. A protocol guarantees delivery. The domain contract determines whether both sides understood the same thing.
I skip the ontology when the domain is small, the cost of a wrong answer is low, or the team is still discovering the product. Formalizing a domain too early can freeze the wrong assumptions into code.
Beyond a wardrobe
I chose clothing because I could check the answer, but the same gap appears in serious systems. The constraint that matters is often the one the data model cannot state.
In clinical software, a medication list contains interactions, contraindications, and effective date ranges. Two records that look alike can mean different things because of when each was recorded.
In access control, a decision depends on clearance, classification, need-to-know, and time. I spent seven years in environments where getting that relationship wrong was more than a bug.
In manufacturing, a part's validity depends on tolerances, materials, and process history that a bill-of-materials row may not carry.
Each case can pass every check the system knows and still fail for a reason it lacks the vocabulary to state.
Write down the world
We have good ways to connect agents. MCP reaches data. A2A reaches agents. Agent Cards describe how to call them. The next responsibility is giving those agents a shared, explicit, checkable account of the world they reason over.
A written-down world lets the system check its work.
Models propose. Contracts dispose.