Appearance
Principle
Types Are Agent Contracts โ
The real question isn't "is this readable?" โ it's "can the next agent even fit the answer in its context window?"
The core tradeoff
How expensive is it to learn the shape of your data? โ
Untyped
O(N)
Read every caller
Shape knowledge is spread across N call sites. At scale, N exceeds the context window. The agent either loads half the codebase or guesses.
Typed
O(1)
Read the type
Shape knowledge is concentrated in one definition. One jump. Fits in any context. Scales to any codebase size.
What the agent actually does
Same question. Two codebases. โ
๐ค Agent in an untyped codebasecost: O(N) ยท may exceed context
โ "What fields does get_user() return?"
1
Open get_user. Signature says -> dict.
No shape info. Keep going.
2
Read function body. Find 3 return statements across branches.
Shapes differ. Which is canonical?
3
Grep callers. 14 matches across 9 files.
Some access .email. Some access .user.id. Inconsistent.
4
Context window at 73%. Still unsure.
Give up. Guess.
๐ฅ Ships:response.user_id โ but actual shape is {"user": {"id": ...}}. Runtime AttributeError in prod.
๐ค Agent in a typed codebasecost: O(1) ยท always fits
โ "What fields does get_user() return?"
1
Open get_user. Signature says -> User.
Jump to User. Read 5 lines. Done.
โ Ships:response.id, response.email. Type-checker confirms before runtime.
Why this matters more than "readability"
Types are the protocol between agents who never meet โ
A pipeline is a chain of context windows. Each one ends.
๐ค
Agent A
writes get_user()
User type
โ
๐ค
Agent B
calls get_user()
Agent B can't ask Agent A anything. It can only read what Agent A left behind.
The type definition is the handoff. A docstring can drift from the code. A type can't โ it's the same artifact.
Named invariants
A literal type is the domain, inline โ
Untyped string
severity: str
# Is it "blocker"?
# "BLOCKER"? "block"?
# Agent must grep
# every producer.
if issue.severity == "blockr":
# โ typo โ silent
escalate()Literal type
severity: Literal[\
"blocker",\
"tech_debt",\
"skippable",\
]
if issue.severity == "blockr":
# โ type-checker
# catches it
escalate()The discipline โ
At system boundaries, everything has a type.
The boundary between an agent and its caller is where context windows end. That's where types stop being hygiene and start being the only way the pipeline works at all.