Why agents need conventions

My agent stopped reading before it reached the part that mattered. This chapter is one file read twice: nothing gets rewritten, and the reading window moves down both files as the page scrolls.

It did reach the line. The line was a bare 30 in the middle of a guard, and nothing anywhere in that file said what 30 was for.

A model does not read your file the way you do. It reads a group of tokens at a time, shifting down as it processes, and what it understands at any moment is whatever is inside that group. The meter above the panes is that group.

The left pane fills with ceremony. A class named for a category rather than a job, an initialize, two assignments, a method called process. The number 30 arrives unlabeled inside a conditional, on the marked line. The window contains the number. It does not contain the policy.

The right pane spends its first window differently. One sentence saying what the class is for, then what it needs, then the company's two numbers under their own heading, and the marked line names the grace period. A reader who sees only that much already knows there is a grace period, how long it is, and that it is a policy someone chose.

What the meter measures is not lines remaining. It is meaning delivered per window. The AED file is the longer of the two, 41 lines against 22, and it still arrives first, because the ordinary file means almost nothing until you have read all of it and assembled the intent in your own head.

billing · two files
Ordinary
AED

Meaning delivered per window · nothing yet, in either fileMeaning delivered per window · the left window bought plumbingMeaning delivered per window · the right file paid in its first windowMeaning delivered per window · 41 lines against 22, and it still arrives first

late_fee_service.cr
1class LateFeeService
2 def initialize(invoice, days_late)
3 @invoice = invoice
4 @days_late = days_late
5 end
6
7 def process
8 if @days_late >= 30
9 fee = calculate_fee(@invoice.amount)
10 @invoice.charge(fee)
11 send_notification(@invoice)
12 end
13 end
14
15 def calculate_fee(amount)
16 amount * 0.05
17 end
18
19 def send_notification(invoice)
20 CustomerMailer.late_fee(invoice).deliver
21 end
22end
apply_late_fee.cr
1# Applies a late fee to an invoice that has gone past its grace period, then tells the customer.
2class Billing:::ApplyLateFee
3 # -- What this needs --
4
5 # The invoice being charged. It knows its customer, its amount, and its due date.
6 property invoice : Invoice
7
8 # How many days past the due date the invoice is today.
9 property how_many_days_late : Int32 = 0
10
11 # -- Company policy --
12
13 # Days past due before any fee applies.
14 property grace_period_in_days : Int32 = 30
15
16 # The fee, as a share of the amount owed.
17 property late_fee_rate : Float64 = 0.05
18
19 # -- How it runs --
20
21 def perform
22 stop_unless_the_invoice_is_overdue
23 charge_the_late_fee
24 tell_the_customer
25 end
26
27 # Nothing happens inside the grace period.
28 private def stop_unless_the_invoice_is_overdue
29 return if how_many_days_late < grace_period_in_days
30 end
31
32 # The fee is a share of the amount owed, charged to the same invoice.
33 private def charge_the_late_fee
34 invoice.charge(invoice.amount * late_fee_rate, kind: :late_fee)
35 end
36
37 # The customer gets the notice by email.
38 private def tell_the_customer
39 CustomerMail.late_fee(invoice).deliver
40 end
41end

Why it matters to the agent

Meaning is assembled from overlapping groups, so every window that holds the name holds the meaning.

The gold outline is the context window: the group of tokens the model is holding at this moment. The tiles are the tokens, and the window moves along the row one token at a time.

Here is the worked example the conventions use. Take an ordinary sentence: I want to create a new user and assign them to an account. Tokenized with Llama 3, that sentence becomes fourteen tokens, and the leading spaces are real: a space is part of a token.

Read it the way a model does. Using an eight-token window that shifts four tokens at a time, so the slices overlap, the sentence is not read once. It is read three times over. A real window is far larger than eight tokens, but the shape is the same at any size.

Apply that to the two files. days_late only carries its meaning if the reader already knows the unit and the subject, days of what and late for what, and both of those live somewhere else in the file, in a different window. how_many_days_late carries the unit and the subject in the name itself, so every window that contains the name contains the meaning.

The same is true of the policy. A bare 30 means whatever the surrounding window says it means; grace_period_in_days : Int32 = 30 means the same thing in every window it ever appears in, including a window that contains nothing else from the file.

Iwanttocreateanewuserandassignthemtoanaccount. Window 1 · tokens 1 to 8 Window 2 · shifted four · tokens 5 to 12 Window 3 · shifted four · tokens 9 to 14
  • Window 1'I want to create a new user and'
  • Window 2' a new user and assign them to an'
  • Window 3' assign them to an account.'
One sentence, fourteen tokens, three overlapping windows. Every token a window shares with the one before it is a token the model reads twice.

Reading with an agent? Hand it the machine edition of this chapter. The source is 01_why_models_need_this.md in the conventions repository.

What one small model measured

Ten pairs, three probes each, graded blind. The gap shows up where the model has to infer.

We ran a small comprehension benchmark to see whether this shows up in practice. Ten pairs of Crystal snippets, one AED variant and one conventional variant each, checked for identical behavior first so the test measured style and not logic. Claude Haiku answered three probes on each variant blind, one for intent, one for modification, one for defect, and a blind grader scored the answers.

comprehension benchmark · scores
AED            60 / 60
conventional   54 / 60
intent         18 / 20
modification   20 / 20   tied
defect-finding 16 / 20

In the report's words: “AED never lost a probe; conventional lost points on 3 of 10 pairs, concentrated in defect-finding (16/20) and intent (18/20) probes, while modification probes tied (20/20 each).”

The shape of that result matters more than the total: “The style seems to matter when the model must infer purpose or notice an edge case, not when it is executing instructions.”

Which is exactly the grace period. Tell an agent to change the fee to six percent and it will manage that in either file. Ask it what the file is for, or whether an invoice twenty-nine days overdue gets charged, and the style starts to decide the answer.

Now the limits, printed here rather than buried, because the report prints them: “A 6-point gap on n=10 with 7 ceiling ties is a directional signal consistent with the hypothesis, not proof of it.”

One small model, one run per probe, no variance estimate, a model doing the grading, snippets rather than a codebase, and pairs authored by the people who authored the conventions. Treat it as a reason to try this on your own code, not as a number to quote at anyone.

And the cost, which the same report names as a threat to its own validity: “AED variants are consistently longer (e.g. p10: 38 vs 21 lines).”

That is true of our two files as well: 41 lines against 22. AED spends lines so that a reader spends fewer windows on inference. If your bottleneck is typing, that trade is a loss. If your bottleneck is an agent that has to work out what your code is for before it can safely change it, it is the trade the whole convention set is built on.

Before and after

Plain attributes, before and after.

Between the two files above, one thing moved and nothing else did. The policy left the method body and became a named property with a sentence above it. The two panes below hold one more class twice: the plain attributes on the left, the stated ones on the right.

customer.cr · before and after
customer.cr · before
1class Customer
2 property name : String
3 property email : String
4 property subscriptions : Array(Subscription) = [] of Subscription
5
6 def initialize(@name, @email)
7 end
8end
customer.cr · after
1class Customer
2 property first_name : String
3 property last_name : String
4 property email_address : String
5 property list_of_all_active_subscriptions : Array(Subscription) = [] of Subscription
6 property is_this_an_enterprise_customer : Bool = false
7
8 def initialize(@first_name, @last_name, @email_address, @is_this_an_enterprise_customer = false)
9 end
10end

subscriptions needs the surrounding file to tell you it is a list and which subscriptions it holds. list_of_all_active_subscriptions tells you inside the name. Chapter 3 takes these naming rules apart one at a time; this chapter only claims the reason they exist.