You build a coding agent harness by starting from a while (true) around one model call and adding one piece at a time, each piece fixing a failure you just watched happen. That’s the workshop I’m teaching at AI Engineer Code Summit in San Francisco on 10 November 2026: an empty loop at 9:00 and a harness you can serve over HTTP by 10:00, with 9 git branches so nobody in the room gets lost. This post is that hour written down.
If you want the definition first, I wrote a whole post on what an agent harness is after my talk Harnesses in AI. That one drives GPT-3.5 Turbo through a browser until it stops lying. This one builds a coding agent, in TypeScript with the Anthropic SDK as the only dependency, on Claude Haiku 5.5 because it’s cheap enough to run live in a workshop.
Step 0: the model remembers nothing #
Here’s the first version in full. One array, one loop, one model call per line you type:
const messages: Anthropic.MessageParam[] = [];
while (true) {
const ask = await terminal.question("\nyou> ");
messages.push({ role: "user", content: ask });
const response = await client.messages.create({ model: MODEL, max_tokens: 16000, messages });
messages.push({ role: "assistant", content: response.content });
}
Tell it your name, restart the process and ask again: it has no idea who you are. The model is a function that takes messages and returns one message (that’s it!!). The array is the memory and the harness owns it.
Look at the last line too: I push all of response.content and never just the text. The newest Claude models sign their thinking blocks and a history that gets edited instead of appended to comes back as an error. So the history only ever grows.
Step 1: one tool and a loop make an agent #
Give it read_file and a second while inside the first one:
while (true) {
const response = await client.messages.create({ model: MODEL, max_tokens: 16000, tools, messages });
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason !== "tool_use") break;
// run every tool_use block, then send ONE user message with every tool_result
messages.push({ role: "user", content: results });
}
That inner loop is the difference between a chatbot and an agent. The model never touches your disk. It asks to read a file and your code decides whether to. People trip up on the results here: every tool_use needs exactly one tool_result (all of them in one user message) and a tool that throws goes back as a result with is_error: true. The model can recover from an error it can see. It can’t recover from one you swallowed.
Step 2: the tool list is the domain #
Now the single tool becomes a registry: a definition the model reads and a function the harness runs, made by one small helper. I add list_files, write_file, edit_file (which refuses unless the text to replace appears exactly once) and bash.
Those 5 tools are what make it a coding agent. Swap them and you have a different agent with the same loop, which comes back in step 7. The descriptions matter more than people think because the model knows only what those strings say. They’re prompts.
Ask it to “add hello.ts that prints today’s date, then run it” and it writes the file and runs it. This is where I expect the room to get a little nervous!
Step 3: a gate before anything you can’t undo #
Bash with no gate means “clean up” can mean rm -rf. So step 3 adds caps and a question for me:
const MAX_STEPS = 25; // model calls per task
const MAX_TOKENS = 300_000; // input plus output tokens per task
const ASK_FIRST = new Set(["write_file", "edit_file", "bash"]);
Before any tool in ASK_FIRST runs, the harness asks me y/N in the terminal. When I say no, the model gets an error result telling it to ask me what to do instead (and it does). The caps make a stuck loop before it runs up a bill. The model proposes and the person decides.
Step 4: every step resends the conversation #
This is the step that surprises people who’ve only used chat apps. Every call sends all of the history again: the system prompt, the tools and every tool result so far. The prompt is the bill.
Claude Haiku 5.5 costs $0.10 per million input tokens, $0.01 per million cached and $0.50 per million out. Past 100,000 tokens in one prompt, the price is 5× for every token in that request, which is insane… one tool result too many and your next call costs 5 times as much. So step 4 caches, cuts and compacts:
- Caches the part that never changes, with one
cache_controlfield. - Cuts any tool result over 12,000 characters where it’s made, with a note telling the model how to ask for less.
- Starts server-side compaction at 90,000 tokens, before the price step and not after it.
const response = await client.beta.messages.create({
model: MODEL,
max_tokens: 16000,
system: SYSTEM,
tools,
messages,
cache_control: { type: "ephemeral" },
betas: ["compact-2026-01-12"],
context_management: { edits: [{ type: "compact_20260112", trigger: { type: "input_tokens", value: 90_000 } }] },
});
The harness also prints what every call costs. On a 3-call job the cached count goes from 0 to 1,040 tokens by the second call and the job costs a tenth of a cent in total.
Step 5: phases beat a bigger model #
If each step works 99% of the time, a 122-step job run as one long loop finishes 0.99<sup>122</sup> of the time (about 29%). Run the same job as short phases with one retry each and every phase finishes about 99% of the time.
I forecast exactly this for long browsing jobs at my startup Daiku: a 104-step job (research across 10 sites, then a purchase) on Haiku 5.5, built from 742 measured task lengths.
Compaction fixes the cost. Only phases fix the success rate!! So in step 5 the loop becomes a function, runAgent(history, system, tools), and the top-level agent gets one more tool:
const phase = tool("run_phase", "Hand one part of the job to a fresh agent with an empty context.", { brief: "..." }, async ({ brief }) => {
return await runAgent([{ role: "user", content: brief }], PHASE, registry);
});
A phase is a whole agent that starts with nothing but its brief and returns a note of at most 5 lines. When I gave it “find the bug in math.ts, then count the lines in package.json”, it ran 2 phases, found that add returns a - b and counted 17 lines, for $0.001 in total.
Step 6: IBM Granite runs the same harness on my laptop #
Ollama serves an Anthropic-compatible endpoint so swapping the model is a different baseURL on the same SDK:
const LOCAL = !MODEL.startsWith("claude");
const client = LOCAL ? new Anthropic({ baseURL: "http://localhost:11434", apiKey: "ollama" }) : new Anthropic();
A local model gets the plain request because caching and compaction are Claude API features. I ran MODEL=granite4:tiny-h with IBM Granite 4 on my Mac, asked how many files were in a folder, and it called list_files and answered 3 correctly. At $0.0000 a call! Nothing else in the harness changed. The harness does more of the work than the model does.
Step 7: the same loop in a different domain #
Everything so far was a coding harness. In step 7 I delete the file and shell tools and replace them with a browser in the cloud that’s already logged in to my sites. These are Daiku’s tools and each one is an HTTPS call with a script key:
daiku("navigate", "Open a URL in the workspace's tab.", { url: { type: "string" } }, ["url", "workspace"]),
daiku("extract", "Read rows from a table or listing as JSON.", { rows: { type: "string" }, fields: { type: "object" } }, ["workspace"]),
Same loop, same caps, same phases. I asked it for the top 3 stories on Hacker News and it opened a workspace, navigated, pulled the titles with one extract and closed the workspace, for $0.0008.
The gate moved too. I took my own y/N gate out because Daiku asks the person on their phone before anything sends, pays or deletes. The gate belongs to whoever owns the risk. Then I added one tool the harness owns: ask_person, a checkpoint for choices that change the result (which shop, which size, which date) so the model asks instead of guessing.
Step 8: the harness as a service #
In the last step the agent goes in agent.ts as createAgent({ client, model, io }) and the terminal is just one front end. The other one is a Cloudflare Worker with a Durable Object per session:
POST /sessions -> { id }
POST /sessions/:id/events -> a stream of the agent's events
GET /sessions/:id/events -> everything that happened so far
The session keeps its history in the Durable Object’s storage so closing the tab or redeploying loses nothing. I ran it locally with Wrangler and it streamed, stored and replayed a whole errand.
If those routes look familiar, it’s because Claude Managed Agents works the same way: an agent, an environment, sessions and events, with Anthropic running the loop and the sandbox. Cloudflare’s Think gives you the same thing as a class you extend (override getModel() and it runs the loop with persistence and resumable streams).
What I’d buy and what I’d write myself #
Having written every step by hand, here’s where I land. I’d happily buy the loop, caching and compaction, sessions that survive a deploy and streams that resume. Those are the same for everyone and both of those products do them well. I’d write my own tool registry and gates and my own phases, checkpoints and budgets. Those are my domain and nobody else can write them for me.
If your team wants to build this by hand on your own agents, that’s my agentic AI workshop.
Questions #
How do you build a coding agent harness?
You build a coding agent harness by starting from a while loop around one model call and adding one piece at a time: a tool, a registry of tools, guardrails, context management, phases and a way to swap the model. Each piece fixes a failure you can watch happen first. Mine is about 200 lines of TypeScript with one dependency, the Anthropic SDK.
What is the difference between an agent loop and an agent harness?
The agent loop calls the model, runs the tool it asked for and feeds the result back until it stops asking. The agent harness is everything around that loop: the tool registry, caps on steps and tokens, gates before anything you can't undo, context management, phases and verification.
Can an agent harness run on a local model?
Yes. Ollama serves an Anthropic-compatible endpoint so the same harness can point the Anthropic SDK at localhost and run IBM Granite on a laptop. Prompt caching and compaction are Claude API features and the harness leaves those fields out of the local request. Granite 4 tiny-h called my file tools and answered correctly at no cost per call.
What is Claude Managed Agents?
Claude Managed Agents is Anthropic's hosted agent harness, in beta since April 2026. You define an agent (model, system prompt, tools, Model Context Protocol (MCP) servers and skills), an environment it runs in, then start sessions and exchange events with it. Anthropic runs the loop, the sandbox, prompt caching and compaction.
Written by me, Tejas Kumar, an AI Engineer at IBM based in Berlin. Read everything else I have written, or go to Fluent React, my O'Reilly book on how React works inside, the talks I give at conferences, and ConTejas Code, my podcast.