An Agent A program that calls a language model in a loop and lets it use tools, such as search or code. that lands a PR Pull request. A proposed code change that others review before it joins the main code. takes between 30 and 60 Turn One step of an agent. It reads the current state, calls the model once, and acts on the reply. to do it. One of those turns is a plan. A handful are edits that matter. The rest are routine: read a file, run a test, fix an import, and run the test again. Now say every one of those turns goes to a Frontier model The largest, most capable and most expensive tier of models. I keep it for the hardest reasoning. tier model, such as GPT-6 Astra, Claude Fable 5.1 or Gemini 3.1 Pro. Then the bill charges frontier prices for grep, a plain text search.
This is the third part of my AI Tokenomics series. Part one covered the eleven rules and the runtime guards around them. Part two covered the spend cap that does not save you from a loop. This part covers the routing decision inside the loop: which tier gets which turn. A mechanical signal decides that, one that my code can check. Then I show what routing does to the cost of a Trajectory One full agent run on one task, from its first turn to its last. at published List price The public price per token that a vendor publishes, before any discount or contract deal..
Anatomy of a 40-turn refactor trajectory
Before I can route turns, I need to know what turns look like. The table shows the shape of a typical refactor run. It is a sketch, not a measurement. In the run, an agent changes a loader's error handling across a small module, and a Pre-commit gate A check that runs the tests on each change. Only exit code 0 lets the change through. runs the tests on each change. The shares are rough. The point is the mix, and most of the mix is cheap, repetitive work.
| Turn type | What happens | Rough share | Tier |
|---|---|---|---|
| Plan | Read the task, pick files, split the work into steps | 1 turn | Frontier |
| Read and navigate | Open a file, list a directory, follow an import | 30% | Workhorse |
| Edit | Apply a small diff to one file | 25% | Workhorse, unless the diff touches a public export |
| Run gate | Run the test or lint command, read the exit code | 20% | Workhorse |
| Repair | Fix whatever the gate complained about | 15% | Workhorse for the first two tries, then Frontier |
| Summarize | Write the PR description | 1 turn | Workhorse |
Two things stand out. First, the plan turn is the only one where the model must hold the whole problem in view, and it happens once. Second, the repair loop is where cheap models get stuck. An expensive model earns its price there, but only after the cheap one has failed in a way the gate can see. Everything else is bookkeeping, and bookkeeping does not need a frontier model.
The router
The router is one function, and it uses nothing beyond the Python standard library. It takes a turn and the state of the trajectory, and it returns a tier. The default is the Workhorse tier. The router escalates a turn to the Frontier tier on four signals:
- The turn is a plan or decompose step, where the model splits the task into steps.
- The pre-commit gate returned Exit code The number a program returns when it ends. 0 means success; any other number, such as 1, means failure. twice in a row for the same file.
- The diff adds, removes or changes a Public signature The name, parameters and return type of a function or class that other code calls.. The check compares the AST Abstract syntax tree. The structure a parser builds from source code, such as functions, classes and signatures. of the source before and after the edit. It is not a Regex Regular expression. A short pattern that matches text, such as every run of digits in a line. on the diff.
- The Workhorse tier output failed Schema A formal description of the shape data must have, such as which fields exist and their types. validation.
I cap escalations per trajectory. A file that fails the gate twice in a row is pinned to the Frontier tier for the rest of the run. Pinned means the router keeps the file there, so it does not send the file back down after one good turn. The cap still outranks the pin. Once the cap runs out, a pinned file drops back to the Workhorse tier like every other turn.
Timing matters too. The router routes plan turns and pinned files before any draft exists. It routes every other turn once the Workhorse tier has written its draft, because the signature and schema checks judge that draft. When the router escalates such a turn, the Frontier tier runs it again. Every decision goes into a log with its reason. A router I cannot audit is a router I will not trust when the bill arrives.
#!/usr/bin/env python3
"""route_turn: pick a tier for one agent turn from mechanical signals only.
Default is the Workhorse tier. The router escalates to the Frontier tier when
(a) the turn is a plan or decompose step,
(b) the pre-commit gate returned exit 1 twice in a row for the same file,
(c) the diff changes a public signature (AST check, not a regex),
(d) the Workhorse output failed schema validation.
Signal (b) pins the file: later edits and repairs on it go to the Frontier tier.
Plan turns and pinned files route before any draft exists. Other turns route once
the Workhorse draft exists, since (c) and (d) judge it. Escalations re-run on Frontier.
Escalations are capped per trajectory, and the cap wins over a pinned file.
A pinned trajectory sends every turn to the Frontier tier, outside the cap.
Every decision is logged with its reason. Standard library only.
"""
import ast
import json
from dataclasses import dataclass, field
WORKHORSE = "workhorse"
FRONTIER = "frontier"
ESCALATION_CAP = 8 # per trajectory; 20 percent of a 40-turn run
FAILURES_BEFORE_PIN = 2 # after this many gate failures a file stays on Frontier
@dataclass
class State:
escalations: int = 0
gate_failures: dict = field(default_factory=dict) # path -> consecutive exit-1 count
pinned: set = field(default_factory=set) # paths pinned to the Frontier tier
log: list = field(default_factory=list) # one dict per routed turn
pinned_trajectory: bool = False # every turn on Frontier, no cap
def _public(name: str) -> bool:
return not name.startswith("_") or (name.startswith("__") and name.endswith("__"))
def public_signatures(source: str) -> dict:
"""Name -> signature for public defs and classes, methods included as Class.method."""
out = {}
def walk(body, prefix):
for node in body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and _public(node.name):
out[prefix + node.name] = (type(node).__name__, ast.dump(node.args),
node.returns and ast.dump(node.returns))
elif isinstance(node, ast.ClassDef) and _public(node.name):
out[prefix + node.name] = "class"
walk(node.body, prefix + node.name + ".")
walk(ast.parse(source).body, "")
return out
def export_change(turn: dict) -> str:
"""Why a Python edit or repair touches a public signature, or '' if it does not."""
if turn["kind"] not in ("edit", "repair") or turn.get("before") is None \
or not str(turn.get("path")).endswith(".py"):
return ""
try:
changed = public_signatures(turn["before"]) != public_signatures(turn.get("after") or "")
except SyntaxError:
return "python source does not parse"
return "diff touches public export" if changed else ""
def route_turn(turn: dict, state: State) -> str:
"""turn = {"n", "kind", "path"?, "before"?, "after"?, "schema_failed"?}."""
kind, path = turn["kind"], turn.get("path")
tier, reason = WORKHORSE, "default"
export = export_change(turn)
if state.pinned_trajectory:
tier, reason = FRONTIER, "pinned trajectory"
elif kind == "plan":
tier, reason = FRONTIER, "plan step"
elif kind in ("edit", "repair") and path in state.pinned:
tier, reason = FRONTIER, "file pinned after repeated repairs"
elif export:
tier, reason = FRONTIER, export
elif turn.get("schema_failed"):
tier, reason = FRONTIER, "workhorse output failed schema"
if tier == FRONTIER and not state.pinned_trajectory:
if state.escalations >= ESCALATION_CAP:
tier, reason = WORKHORSE, "cap %d reached, stayed on workhorse" % ESCALATION_CAP
else:
state.escalations += 1
state.log.append({"turn": turn["n"], "kind": kind, "tier": tier, "reason": reason})
return tier
def observe_gate(turn: dict, state: State, exit_code: int) -> None:
"""Feed a gate result back; a second failure in a row pins the file."""
path = turn.get("path")
if path is None: # a whole-suite run names no file to pin
return
if exit_code == 0:
state.gate_failures.pop(path, None)
return
state.gate_failures[path] = state.gate_failures.get(path, 0) + 1
if state.gate_failures[path] >= FAILURES_BEFORE_PIN:
state.pinned.add(path)
def dump_log(state: State) -> str:
return "\n".join(json.dumps(entry, separators=(",", ":")) for entry in state.log)The decision log holds one JSON A common text format for structured data. A parser needs the whole object; half of one fails. object per line. That format is boring on purpose. I can grep it, and I can load it into a spreadsheet. After the run, it tells me why a given turn cost what it cost.
{"turn":1,"kind":"plan","tier":"frontier","reason":"plan step"}
{"turn":2,"kind":"read","tier":"workhorse","reason":"default"}
{"turn":18,"kind":"edit","tier":"frontier","reason":"diff touches public export"}
{"turn":32,"kind":"repair","tier":"frontier","reason":"file pinned after repeated repairs"}
{"turn":38,"kind":"edit","tier":"workhorse","reason":"cap 8 reached, stayed on workhorse"}The video below runs the router over a scripted 40-turn trajectory. It prints every decision, then prices the run three ways. The trajectory is a Fixture A fixed, scripted input for a test or demo. It plays out the same way every time., not a recording of a live agent. What the video shows is the router doing what the code says it does.
Get the next essay by email
One email per new essay: a production failure, taken apart, and the code that fixed it. Reply to any of them to unsubscribe.
Three numbers at list prices
I priced this at published list prices. It is not a report from a live run. The prices are the vendors' list prices as of the catalog's date, 27 September 2026, and they change. The lab at the end of the post computes everything again from your own inputs. So treat the numbers here as the shape of the answer, not the answer itself.
These are the lab's defaults. Each turn sends 32,000 prompt Token A small piece of text, often part of a word. Models read, write and bill text by the token. and gets 2,000 output tokens back. The hit rate in the KV cache The attention keys and values a model saved for tokens it already read. Reusing them skips recomputing that prefix. is 50%, so half of the prompt is already cached. A trajectory has 40 turns, and the router escalates 20% of them. The cost of one turn has three parts. The half of the prompt that misses the cache bills at the uncached input price. The half that hits bills at the cheaper cached input price. The output tokens bill at the output price. Figure 2 adds up those three parts for one turn, then for the whole run.
One turn: 32,000 tokens in, 2,000 out
On the Frontier tier: $0.0592 a turn
On the Workhorse tier: 2.9x cheaper
The router: 8 Frontier turns, 32 Workhorse
Take Gemini 3.1 Pro as the Frontier tier. It is still a Preview model, gemini-3.1-pro-preview. Take Gemini 3.8 Flash as the Workhorse tier. A Frontier tier turn costs $0.0592, and a Workhorse tier turn costs $0.0207. From there:
| Policy | Per turn | Per 40-turn trajectory | Per 1,000 trajectories |
|---|---|---|---|
| All Frontier | $0.0592 | $2.37 | $2,368 |
| All Workhorse | $0.0207 | $0.83 | $828 |
| Router, 20% escalation | $0.0284 | $1.14 | $1,136 |
The router costs 52% less than all-Frontier, and it still sends one turn in five to the expensive tier. It costs about $300 more per thousand trajectories than all-Workhorse. That gap is what the escalations buy: a frontier model on the plan, on the public-signature edit, and on the repair loop that the cheap model could not close. One detail: the table prices an escalated turn at the Frontier rate only. A turn that the signature or schema check escalated also paid for the Workhorse draft it rejected. In this run that is one extra Workhorse turn, about $0.02.
The Gemini numbers carry an expiry date. Google lists the Flash rates as promotional through 31 December 2026. From 1 January 2027 they double, to $1.50 input, $0.15 cached and $7.50 output per million tokens. A Workhorse turn then costs $0.0414. The price ratio between the two tiers falls from 2.9x to 1.4x. The same routed run then costs $1.80 against $2.37 for all-Frontier, a saving of 24%.
Next, the same arithmetic with the other two pairs. What changes is the price ratio between the tiers. The ratio is the whole story. So for each pair I give the ratio and the three trajectory costs, and nothing else.
| Pair | Frontier to Workhorse ratio | All Frontier | All Workhorse | Router, 20% |
|---|---|---|---|---|
| Gemini 3.1 Pro / Gemini 3.8 Flash | 2.9x | $2.37 | $0.83 | $1.14 |
| Claude Fable 5.1 / Claude Haiku 4.5 | 9.6x | $10.56 | $1.10 | $3.00 |
| GPT-6 Astra / GPT-5.6 Luna | 46.6x | $11.04 | $0.24 | $2.40 |
Read down the last column, not across the rows. The wider the price gap between a vendor's two tiers, the more a 20% escalation rate costs compared with all-Workhorse. The same wide gap makes it save more compared with all-Frontier. At a 2.9x ratio the router saves 52%. At a 46.6x ratio it saves 78%, and the 20% of turns that escalate make up 92% of the routed bill. That last number is why I keep the escalation rate honest. I pay for every point of escalation that no signal justifies, and I pay at the wide end of the ratio.
Where it breaks
Three failure modes follow. The router or the lab already has a fix for each one.
Cascade thrash. The Workhorse tier fails the gate, so the router escalates. The Frontier tier fixes the file. For the next edit on the same file, the router drops back to the Workhorse tier, and the gate fails again. With no cap, this loop pays for both tiers on every cycle. So the router caps escalations per trajectory. It also pins a file to the Frontier tier after the file's second gate failure in a row. The third attempt on a hard file goes to the Frontier tier and stays there until the cap runs out.
KV-cache prefix loss on a tier switch. The cached input price assumes that the start of the prompt, its prefix, is already stored on the model that serves the turn. After a tier switch, the other model has never seen that prefix. So the first turn after a switch is a cold prompt, billed at that tier's uncached rate. At the lab defaults, a cold Frontier tier turn on Gemini 3.1 Pro costs $0.0880 instead of $0.0592. The lab's cold-cache preset sets the hit rate to zero, so you can see the worst case for a router that switches often. If your escalations come in clusters, the cost sits between the warm and cold numbers. If they alternate turn by turn, you are close to cold.
Trajectories that should never be routed. Some runs deserve the Frontier tier from the first turn. One is greenfield design: new code, with no gate to fail yet. Another is a security-sensitive change, where the failure is a wrong edit that passes the tests. A third is a refactor across several repositories, where the plan must hold across contexts the Workhorse tier will not see. For these I pin the whole trajectory, and the router has a mode for that. Set pinned_trajectory on the state, and every turn returns Frontier with the reason "pinned trajectory", outside the cap.
Latency moves the same way as cost. A Workhorse tier turn returns faster, in wall-clock time, than a Frontier tier turn on the same prompt. How much faster depends on your region, your prompt size and the hour. Measure it on your own traffic and enter the numbers into the lab, rather than taking a figure from me.
Lab: your prices, your trajectory
The lab below computes the three numbers again from your own settings. It opens on the model pair, the escalation rate and the cache hit rate. Under More settings you can also change the prompt size, the output size and the turn count, pick each tier's model yourself, and enter your own latency. It uses the catalog list prices for the models you pick. Four presets match the sections above. all-frontier is the baseline. router-20 is the run I walk through above. router-5 is what a tighter set of signals buys. cold-cache is the worst case after a tier switch, with the hit rate at zero.
Compare three ways to run one agent trajectory at published per-token list prices: every turn on the Frontier tier, every turn on the Workhorse tier, or a router that keeps the Workhorse tier by default and escalates a chosen share of turns. Pick one of three same-vendor model pairs, then move the escalation share and the KV-cache hit rate. More settings holds one model per tier, custom rates, token sizes, turn count and latency.
One reading of the same idea comes from the research side. Pro-Router: Token-Aware Progressive Model Routing (Aug 2026) by Gui and co-authors frames routing as a progressive decision made with the token budget in view, rather than a one-shot classifier. That is the same instinct as my repair-count and cap rules. Their router learns the signal; mine hard-codes it. For a pre-commit loop I would rather have the hard-coded one, because I can read it.
What to change this week
- Flip the default. Route every turn to the Workhorse tier, and make the plan turn the only escalation. Watch the gate pass rate for a day before you add the other three signals.
- Write the decision log before you write the router. Log one JSON line per turn, with the tier and the reason. If the reason field is ever empty or says "model asked for it", that is the bug.
- Put the escalation cap in the same config file as your spend cap. They are the same control at two different layers. The spend cap is the one that fires when the escalation cap is set wrong.