{"slug": "how-to-build-a-coding-agent-harness-from-an-empty-loop", "title": "How to Build a Coding Agent Harness From an Empty Loop", "summary": "A workshop at the AI Engineer Code Summit in San Francisco on 10 November 2026 will teach attendees to build a coding agent harness in TypeScript on Claude Haiku 5.5, starting from a bare `while (true)` loop around a single Anthropic SDK model call and adding one component at a time, each fixing an observed failure. The build proceeds through 9 git branches, adding a `read_file` tool and inner tool-use loop, then a 5-tool registry (`read_file`, `list_files`, `write_file`, `edit_file`, `bash`), then caps of 25 model calls and 300,000 tokens per task with a confirmation gate before irreversible actions. The author notes that the newest Claude models sign their thinking blocks, so message history must only be appended to, never edited, or the API returns an error.", "body_md": "# How to Build a Coding Agent Harness From an Empty Loop\n\n**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](https://www.ai.engineer/code/2026) 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.\n\nIf you want the definition first, I wrote a whole post on [what an agent harness is](https://tej.as/blog/what-is-an-agent-harness) after my talk [Harnesses in AI](https://tej.as/talks/ai-engineer-europe-2026). That one drives [GPT-3.5 Turbo](https://platform.openai.com/docs/models/gpt-3.5-turbo) through a browser until it stops lying. This one builds a *coding* agent, in [TypeScript](https://www.typescriptlang.org) with the [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-typescript) as the only dependency, on [Claude Haiku 5.5](https://platform.claude.com/docs/en/about-claude/models/overview) because it’s cheap enough to run live in a workshop.\n\n## Step 0: the model remembers nothing\n\nHere’s the first version in full. One array, one loop, one model call per line you type:\n\n``` js\nconst messages: Anthropic.MessageParam[] = [];\n\nwhile (true) {\n  const ask = await terminal.question(\"\\nyou> \");\n  messages.push({ role: \"user\", content: ask });\n  const response = await client.messages.create({ model: MODEL, max_tokens: 16000, messages });\n  messages.push({ role: \"assistant\", content: response.content });\n}\n```\n\nTell 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.\n\nLook 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.\n\n## Step 1: one tool and a loop make an agent\n\nGive it `read_file` and a second `while` inside the first one:\n\n``` js\nwhile (true) {\n  const response = await client.messages.create({ model: MODEL, max_tokens: 16000, tools, messages });\n  messages.push({ role: \"assistant\", content: response.content });\n  if (response.stop_reason !== \"tool_use\") break;\n  // run every tool_use block, then send ONE user message with every tool_result\n  messages.push({ role: \"user\", content: results });\n}\n```\n\nThat 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.\n\n## Step 2: the tool list is the domain\n\nNow 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`.\n\nThose 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.\n\nAsk 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!\n\n## Step 3: a gate before anything you can’t undo\n\nBash with no gate means “clean up” can mean `rm -rf`. So step 3 adds caps and a question for me:\n\n``` js\nconst MAX_STEPS = 25; // model calls per task\nconst MAX_TOKENS = 300_000; // input plus output tokens per task\nconst ASK_FIRST = new Set([\"write_file\", \"edit_file\", \"bash\"]);\n```\n\nBefore 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 pause before it runs up a bill. **The model proposes and the person decides.**\n\n## Step 4: every step resends the conversation\n\nThis 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.\n\nClaude Haiku 5.5 costs [$0.10 per million input tokens, $0.01 per million cached and $0.50 per million out](https://platform.claude.com/docs/en/about-claude/pricing). 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:\n\n1. Caches the part that never changes, with one `cache_control` field.\n2. Cuts any tool result over 12,000 characters where it’s made, with a note telling the model how to ask for less.\n3. Starts [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction-threshold) at 90,000 tokens, before the price step and not after it.\n\n``` js\nconst response = await client.beta.messages.create({\n  model: MODEL,\n  max_tokens: 16000,\n  system: SYSTEM,\n  tools,\n  messages,\n  cache_control: { type: \"ephemeral\" },\n  betas: [\"compact-2026-01-12\"],\n  context_management: { edits: [{ type: \"compact_20260112\", trigger: { type: \"input_tokens\", value: 90_000 } }] },\n});\n```\n\nThe 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.\n\n## Step 5: phases beat a bigger model\n\nIf 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.\n\nI forecast exactly this for long browsing jobs at my startup [Daiku](https://daiku.ai): a 104-step job (research across 10 sites, then a purchase) on Haiku 5.5, built from 742 measured task lengths.\n\nCompaction 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:\n\n``` js\nconst phase = tool(\"run_phase\", \"Hand one part of the job to a fresh agent with an empty context.\", { brief: \"...\" }, async ({ brief }) => {\n  return await runAgent([{ role: \"user\", content: brief }], PHASE, registry);\n});\n```\n\nA 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.\n\n## Step 6: IBM Granite runs the same harness on my laptop\n\n[Ollama](https://ollama.com) serves an [Anthropic-compatible endpoint](https://docs.ollama.com/api/anthropic-compatibility) so swapping the model is a different `baseURL` on the same SDK:\n\n``` js\nconst LOCAL = !MODEL.startsWith(\"claude\");\nconst client = LOCAL ? new Anthropic({ baseURL: \"http://localhost:11434\", apiKey: \"ollama\" }) : new Anthropic();\n```\n\nA local model gets the plain request because caching and compaction are Claude API features. I ran `MODEL=granite4:tiny-h` with [IBM Granite 4](https://ollama.com/library/granite4) 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.**\n\n## Step 7: the same loop in a different domain\n\nEverything 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:\n\n```\ndaiku(\"navigate\", \"Open a URL in the workspace's tab.\", { url: { type: \"string\" } }, [\"url\", \"workspace\"]),\ndaiku(\"extract\", \"Read rows from a table or listing as JSON.\", { rows: { type: \"string\" }, fields: { type: \"object\" } }, [\"workspace\"]),\n```\n\nSame loop, same caps, same phases. I asked it for the top 3 stories on [Hacker News](https://news.ycombinator.com) and it opened a workspace, navigated, pulled the titles with one `extract` and closed the workspace, for $0.0008.\n\nThe 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.\n\n## Step 8: the harness as a service\n\nIn 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](https://developers.cloudflare.com/workers/) with a [Durable Object](https://developers.cloudflare.com/durable-objects/) per session:\n\n``` php\nPOST /sessions                 -> { id }\nPOST /sessions/:id/events      -> a stream of the agent's events\nGET  /sessions/:id/events      -> everything that happened so far\n```\n\nThe session keeps its history in the Durable Object’s storage so closing the tab or redeploying loses nothing. I ran it locally with [Wrangler](https://developers.cloudflare.com/workers/wrangler/) and it streamed, stored and replayed a whole errand.\n\nIf those routes look familiar, it’s because [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) works the same way: an agent, an environment, sessions and events, with Anthropic running the loop and the sandbox. [Cloudflare’s Think](https://developers.cloudflare.com/agents/harnesses/think/) gives you the same thing as a class you extend (override `getModel()` and it runs the loop with persistence and resumable streams).\n\n## What I’d buy and what I’d write myself\n\nHaving 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.\n\nIf your team wants to build this by hand on your own agents, that’s [my agentic AI workshop](https://tej.as/workshops).\n\n## Questions\n\n### How do you build a coding agent harness?\n\nYou 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.\n\n### What is the difference between an agent loop and an agent harness?\n\nThe 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.\n\n### Can an agent harness run on a local model?\n\nYes. 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.\n\n### What is Claude Managed Agents?\n\nClaude 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.\n\nWritten by me, Tejas Kumar, an AI Engineer at IBM based in Berlin. Read [everything else I have written](https://tej.as/blog), or go to [Fluent React, my O'Reilly book on how React works inside](https://tej.as/react), [the talks I give at conferences](https://tej.as/speaking), and [ConTejas Code, my podcast](https://tej.as/podcast).", "url": "https://wpnews.pro/news/how-to-build-a-coding-agent-harness-from-an-empty-loop", "canonical_source": "https://tej.as/blog/how-to-build-a-coding-agent-harness/", "published_at": "2026-11-04 09:00:43+00:00", "updated_at": "2026-11-04 09:19:47.850628+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models"], "entities": ["AI Engineer Code Summit", "Anthropic", "Claude Haiku 5.5", "TypeScript", "Anthropic SDK", "San Francisco"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-build-a-coding-agent-harness-from-an-empty-loop", "markdown": "https://wpnews.pro/news/how-to-build-a-coding-agent-harness-from-an-empty-loop.md", "text": "https://wpnews.pro/news/how-to-build-a-coding-agent-harness-from-an-empty-loop.txt", "jsonld": "https://wpnews.pro/news/how-to-build-a-coding-agent-harness-from-an-empty-loop.jsonld"}}