Skip to content

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.