Part 8: Context engineering
Agents guess at schema they were not shown
A model adds a duplicate column when the context did not carry the existing one.
A bug report that teams running coding agents against a real product tend to meet: The agent added a user_email column to a table that already had email. Or wrote a migration duplicating an index. Or built a query against a column renamed two quarters ago. The postmortem instinct is to call it a hallucination. The mechanism is duller: the model was not shown the schema, so it did what an engineer with no view of the database would do, which is guess.
Why schema is the worst-served context type#
Schema knowledge resists text retrieval. The live truth is not in any one file. It is the fold of an ordered migration history: 400 migration files where the current shape of orders is the sum of 23 of them, interleaved with everything else. Grep for the table name and you get all 23 plus every query that touches it, which are closely related distractors, the kind of noise part 7 showed does the most damage. Object-relational mapper (ORM) models drift from the database. Column semantics (amount is integer cents, not a float) live in tribal memory. Telling the agent to read the migrations folder does not fix this, because what it needs is a computed state rather than a document.
Schema becomes another graph node#
The deterministic fix treats schema the way part 4 treats code, as structure to be indexed rather than text to be searched.
- Schema index: introspect the live database (or replay migrations) into a canonical, versioned snapshot of tables, columns, types, constraints, indexes, and foreign keys. Most ORMs ship the machinery.
- Migration graph: each column carries its lineage, created in
0042, renamed in0187, backfilled in0201. "Why is this nullable?" becomes a graph query with commit-linked answers. - Code and schema edges: static analysis of ORM models and query sites links
LedgerEntrytoledger_entriesto the 14 call sites that write it. "What breaks if I drop this column?" becomes a traversal rather than a guess. - Domain mapping: tables attach to the part 5 domains, so a billing task retrieves billing tables and only billing tables.
ctx = schema.slice_for(anchors=["RefundProcessor"])
# orders(id, user_id→users.id, amount_cents INT NOT NULL, status ENUM,...)
# refunds(id, order_id→orders.id, amount_cents INT, reason TEXT,...)
# lineage: refunds.amount_cents renamed from amount in 0187 (PR #3122)
# invariant: money columns are integer cents (lint: no-float-money)
# ~600 tokens. Verified against production at SHA 9f31c2.
A slice of a few hundred verified tokens replaces the migration files the agent would otherwise read. Because the slice comes from the live database, the duplicate-column failure also gets a second line of defence. The deterministic layer diffs the proposed migration against the index and rejects collisions before a human reviews them. That is the pattern of this book at small scale. Do not ask the model to remember reality. Show it reality, then check its output against reality.
The check that catches the duplicate column#
$ schema-check migrations/0203_add_user_email.py
# reject: users.user_email collides with users.email (created 0042)
# lineage: users.email created 0042, indexed 0091, no rename since
# hint: 3 call sites already read users.email
# 1 collision, 0 warnings. exit 1
Run it in the same CI job that lints migrations. The failure names the column, the migration that created it, and the call sites that depend on it, which is enough for the agent to correct itself on the next turn.
Introspect the database, or replay the migrations#
- Introspect a restored copy when production is the source of truth and hand-run DDL exists. You get the real shape, including the index somebody added by hand. The cost is a restore in CI.
- Replay migrations into a scratch database when the migration history is authoritative and complete. It is cheaper and reproducible from the repository alone, but drift between the files and production stays invisible.
Pick one and stamp the snapshot with the SHA it came from. An unstamped snapshot becomes the stale wiki of part 5, with worse consequences.
The general principle holds even where the schema-specific study does not exist yet. Retrieval grounding reduces fabrication compared with parametric recall, the precision of what is retrieved dominates its volume (the distractor results in parts 2 and 77,9), and structured representations of a system outperform prose descriptions of it for machine consumption (parts 4 to 612,13). Schema is the highest-stakes instance. It is the context type where a fabricated column becomes a migration that someone has to reverse.
Synthesis. See refs 7, 9, 12, 13
- Produce one snapshot. Run your ORM's introspection (SQLAlchemy reflection, Django
inspectdb, Prisma or Drizzle introspect) against a restored copy or a replayed scratch database. The artifact isschema.jsonwith tables, columns, types, constraints, indexes, foreign keys, and the SHA it was built from. - Slice it by anchor. Given a symbol or a path, return the tables that symbol touches plus their direct foreign keys. Done looks like the 600-token slice above for your busiest table.
- Add the collision check. Diff any proposed migration against the snapshot and fail on a column whose name, or an obvious variant of it, already exists on that table. Done looks like the check rejecting a migration you write on purpose.
- Link code to tables. A static pass over ORM model definitions and raw query sites gives you table to symbol to call site. That index answers the drop-column question without anyone reading 400 files.
- Refresh the snapshot where migrations run. Same job, same commit, so the stamp stays honest.
schema_slice_tokens: tokens of schema context per task. Lower is better, with a few hundred as the working target. Compare it against the token count of the migration files the agent used to read.schema_snapshot_age: migrations applied since the snapshot's stamped SHA. Lower is better, and 0 is reachable once step 5 lands.collision_rejections: proposed migrations the check rejects per week. Watch the direction. A count that falls while agent volume holds steady means the slice is doing its job upstream.unmapped_query_sites: query sites the static pass could not link to a table. Lower is better, and each one is a place where the drop-column answer is still a guess.
Your database's current shape is computable, versionable, and sliceable. If the agent guesses about schema, that is a missing index in your system rather than a failure of the model.
Cite this
Anderson, M. (2026). Agents guess at schema they were not shown. In Engineering Deterministic AI Coding Agents (2nd ed., Part 8). Oxagen Inc. https://macanderson.com/manual/show-the-agent-the-schema
BibTeX
@incollection{anderson2026showtheagent,
author = {Anderson, Mac},
title = {Agents guess at schema they were not shown},
booktitle = {Engineering Deterministic AI Coding Agents},
edition = {Second},
chapter = {8},
publisher = {Oxagen Inc.},
address = {Los Angeles, CA},
year = {2026},
url = {https://macanderson.com/manual/show-the-agent-the-schema}
}Updates by email
Get the next edition of the field manual and new research when it is published.