Use an Expensive Model to Build a Cheap Agentic Runtime

James Phoenix
James Phoenix

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.py 50 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.

Leanpub Book

Read The Meta-Engineer

A practical book on building autonomous AI systems with Claude Code, context engineering, verification loops, and production harnesses.

Continuously updated
Claude Code + agentic systems
View Book

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

References

Topics
Agent ArchitectureAgent OrchestrationAgent SkillsCost Optimization

Newsletter

Become a better AI engineer

Weekly deep dives on production AI systems, context engineering, and the patterns that compound. No fluff, no tutorials. Just what works.

Join 306K+ developers. No spam. Unsubscribe anytime.


More Insights

Cover Image for Simulate Users Over Time to Find the Bugs Integration Tests Cannot See

Simulate Users Over Time to Find the Bugs Integration Tests Cannot See

A green integration suite proves the product survives one request. It says nothing about whether the product survives a month.

James Phoenix
James Phoenix
Cover Image for Verified Spec-Driven Development (VSDD)

Verified Spec-Driven Development (VSDD)

Three methodologies that usually get argued about as rivals turn out to compose. Spec-driven development defines what, test-driven development enforces how, and adversarial verification checks that nothing was missed. Run them as sequential gates instead of competing philosophies and you get a pipeline where every line of code can be traced back to the requirement that demanded it.

James Phoenix
James Phoenix