The model that writes the harness and the model that runs inside it do not have to be the same model. They probably should not be.
Author: James Phoenix | Date: September 2026
Two Model Slots, Not One
When I write an Agent SDK script there are two model decisions hiding inside one word. There is the model that designs the pipeline, and there is the model that gets invoked N times once the pipeline is running. Most people pick one tier and use it for both.
Authoring is a one-off. Runtime is a loop. A better authoring model produces a better artefact at fixed cost, while a worse runtime model costs you on every single invocation. So spend everything you have on the artefact and as little as possible on the execution.
Which reframes what you are actually building. The deliverable is not a script that gets a job done. The deliverable is a runtime in which a cheap model is sufficient.
Build Small, Domain-Specific Agents
This only works because of what I am building. A general-purpose agent needs a strong model, and no amount of harness work changes that: the space it operates in is open, so the next input can always be a shape you did not anticipate, and the model has to supply the judgement in the moment.
A small domain-specific agent is the opposite. One document type, one question, one output shape. A bounded space is what makes the harness able to absorb the work, because a failure class you fix today stays fixed tomorrow rather than reappearing in a form you have never seen.
So the Agent SDK is not being used here to build an assistant. It is being used to build one narrow thing that does one job over one kind of input, and then another one next to it. Where a general agent would branch, I write two agents. Where it would need a tool to go and find something, the harness has already fetched it. The domain vocabulary, the valid output shapes, the routing between item types: all of that is code in the runtime, not context the model has to hold.
The corollary is the honest limit on the pattern. If you cannot describe the agent’s job in one sentence, you are not going to get it onto a cheap model, and the fix is to split it rather than to keep hardening.
What a Cheap Runtime Looks Like
The expensive model never appears in the script. It sits one level up, in the session where I am building the thing:
You Claude Code / Codex agent.py + Agent SDK
| claude-opus-5 claude-haiku-4-5
| | |
| "build me an | |
| agent for X" | |
|---------------------->| writes the script -->|
| | |
| "run it 50x, fix | |
| the failure modes" | |
|---------------------->| runs it ------------>| 50 invocations
| |<-- failing outputs ---|
| | patches the script ->|
| | (repeat) |
So the only model the file knows about is the cheap one:
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock
RUNTIME_MODEL = "claude-haiku-4-5" # the only model this file invokes
OPTS = ClaudeAgentOptions(
model=RUNTIME_MODEL,
system_prompt=SYSTEM,
allowed_tools=[], # the harness already did the tool work
max_turns=1, # one question, one answer, no exploring
)
async def solve(prompt: str) -> str:
parts = []
async for msg in query(prompt=prompt, options=OPTS):
if isinstance(msg, AssistantMessage):
parts += [b.text for b in msg.content if isinstance(b, TextBlock)]
return "".join(parts)
allowed_tools=[] and max_turns=1 are not thrift, they are the design. Every tool you leave enabled is a decision you have handed back to the runtime model, and every extra turn is a chance for it to wander. A cheap runtime is one where the model has been given no room to be clever, because everything that needed cleverness already happened in Python.
The Cheap Model Is a Probe
You do not arrive at those options by guessing. You downgrade the runtime model and watch things break.
A strong runtime model absorbs your design mistakes silently. Hand it a half-parsed document and it infers the structure. Give it three tasks in one prompt and it does all three. Feed it an ambiguous schema and it picks a reasonable interpretation, which is exactly why you never find out the schema was ambiguous.
A weak runtime model does the literal thing, and when the literal thing is not enough it fails loudly and in a specific place.
Every one of those failures is a line of code you have not written yet. Not a prompt you have not tuned. That is where this diverges from ordinary model downgrade testing: when a skill fails on a small model the remedy is clearer prose, when an SDK script fails the remedy is to stop asking the model at all and do the work in code before the call.
Concretely, the fix always looks like moving a line out of the prompt and into Python:
# before: the prompt is carrying the pipeline
await solve(f"Here is a document. Find every item, work out which ones "
f"have diagrams, then check each answer.\n\n{raw}")
# after: the harness carries the pipeline, the model answers one question
for item in extract_items(raw): # deterministic parse
if item.has_figure: # structural, not a judgement call
continue # routed to a different task
verdict = await solve(render(item))
The Ratchet
You do not drive this by hand. Hand the authoring model the script and tell it to patch itself recursively. Three steps, repeated:
1. sample from the space 50-100 outputs, spread across the input distribution
2. show the expensive model the failures, with the inputs that produced them
3. refine the script patch the harness so that class cannot recur
-> resample, repeat until the sample is clean
In practice I never write a driver for this. I prototype inside Claude Code or Codex on the most performant model available, have it write the first version of the custom agent, and then hand it the loop as a prompt:
Run
scripts/agent.py50 times across a sample spanning every input type. Collect the outputs that disagree with the expected answer, group them into failure classes, then rewrite the Agent SDK script until each class cannot recur. Fix the harness, not the system prompt. Repeat until the sample is clean.
That is the entire driver. The expensive model is doing the sampling, the diagnosis and the patching, and the only thing it is not doing is answering the actual questions. It runs the script, reads its own failures, edits the file, runs it again. I read the diff at the end rather than supervising each pass.
Step 1 is the one people get wrong. The first 50 rows of your data are all the same shape, and a sample that misses a region of the space cannot tell you the harness is missing code for it. Sample across the space, not off the top of it. Size matters too: one bad output is noise, while fifty is enough for a failure class to repeat, and the repetition is what turns a single wrong answer into a specification for a piece of code.
async def sweep(sample: list[Item]) -> list[Failure]:
got = await asyncio.gather(*(solve(render(i)) for i in sample))
return [Failure(i, g, i.expected) for i, g in zip(sample, got) if g != i.expected]
Step 3 is where the discipline lives. The authoring model, handed a list of failures, will reach for the prompt every time, because editing a string is the cheapest thing in front of it. Say it explicitly in the instruction: patch the Agent SDK or Codex SDK script, not the system prompt. Each pass should strip something out of the prompt and put it into code. Parsing, routing, batching, retries, validation, aggregation. None of that is intelligence, it is plumbing, and plumbing belongs in the runtime where it is deterministic, testable and free. You do not make the cheap model smarter, you make the question dumber.
The i.expected in that snippet is the prerequisite. You need a set of inputs whose correct answers you already know, or you cannot tell “the cheap model failed” from “this input is genuinely hard”, and the diagnostic collapses into guesswork.
Key Insight
Treat a cheap runtime model as a specification test for your harness. It cannot compensate for your design, so wherever it fails, your design was leaning on the model to cover for missing code. Fix the code, not the prompt.
The bill going down is the symptom. The runtime getting stronger is the point.
Related
- Model Downgrade Testing Hardens Agent Skills – The same signal applied to skill instructions rather than SDK harnesses
- Building the Harness – What the logic gets pushed down into
- Ad-hoc Flows to Deterministic Scripts – Agents for decisions, scripts for execution
- Autonomous Loops Need Benchmarks – Why the known-answer set comes first
- Domain Glossary as Agent Constraint – Putting the domain vocabulary in the runtime rather than the prompt
- Sub-agent Architecture – Splitting one broad agent into several narrow ones
References
- Claude Agent SDK documentation –
query(),ClaudeAgentOptions, tools, subagents and sessions

