PHASE 12 · HARNESS
Goal: Understand what production agent frameworks actually do for you.
The agent harness is everything around the model — tool dispatch, retry/backoff, context management, streaming output, interrupt handling, permission prompts, session persistence, pre/post-tool hooks. Frameworks like LangGraph, Strands, and Claude Code are harnesses.
Understanding what a harness provides (and what you've been doing manually) is the difference between "I wrote an agent" and "I have a production agent system." Most of the engineering complexity in agentic apps lives in the harness, not the model.
The final exercise: compare your hand-rolled agent loop side-by-side with a real harness like Claude Code or LangGraph. What did they reinvent? What's worth borrowing? What did you do better because you understood the underlying problem?
before_tool, after_tool, on_error), a permission system, and session state.harness.md — A feature comparison + writeup of harnesses you've used, plus one feature added to your own loop.
Concepts unlocked: the model vs. the system around it · production complexity · build vs. buy
Back in Phase 4, we called the code that carries messages and runs tools "the harness," and the heart of it "the loop." Now look at everything else you've built since: retries when a call fails, capping the loop, trimming context, saving memory, wrapping guardrails, tracing every call. Notice that almost none of that is the model — it's all machinery around the model.
One thing to get straight before we widen the lens: the loop didn't get renamed. Your Phase 4 loop is still there, still doing exactly the same job — carry the question in, run the tools, carry the answer out. It's just one component inside a bigger harness. The word didn't change; the picture got wider: loop + streaming + permissions + session state + everything else in this phase.
Three of those words are new, so here they are in plain terms before you meet them in a table. Streaming — the answer appears word by word as the model writes it, instead of all at once at the end (you've watched ChatGPT do this every time you've used it). Permissions — the harness stops and asks you before running a risky tool, like sending an email or deleting a file. Session persistence — close your laptop mid-conversation, come back tomorrow, and the conversation is still there. That last one should sound familiar: it's your Phase 8 memory work — saving and re-loading session state — wearing its production name.
Here's the capstone realization: look at that right-hand column again — you have already built most of it. The loop is your Phase 4 agent.py. Context management is your Phase 8 compaction. Session persistence is your Phase 8 memory.py. Observability — recording what every call did and cost — is your Phase 6 tracing. And your Phase 7 guardrails? They attach to the harness through hooks, which you'll meet in a moment. The harness isn't a new thing to learn. It's the building this whole course has been constructing, one room at a time.
Frameworks like LangGraph, Strands, and even Claude Code itself are harnesses — someone else's version of that same building. Understanding what a harness does is the difference between "I wrote an agent loop" and "I have a production agent system."
What a harness is. The harness is everything around the model: the loop (tool dispatch), retries, context management, streaming, permissions, session state, hooks. The Phase 4 loop is one component of it — the one at the center. Most of the engineering complexity in agentic apps lives in the harness, not the model.
In the intuition we listed seven jobs a harness handles. Here they are one by one — with a tag showing where in this course you already did each one by hand:
Notice that three of those — retries, streaming, permissions — never got a "your Phase X work" tag. You never built them, on purpose. Hold that thought; it's the crux of the next section.
Now, these seven don't sit side by side like drawers in a cabinet. They fire in order, at different moments of a single request, with the loop at the center and everything else wrapped around it. Follow one message all the way through:
Read the numbers: the session loads before the loop starts, context gets trimmed before the model sees it, permissions and hooks fire between the model asking for a tool and the tool actually running, and streaming happens on the way out. One request, seven components, each at its moment.
The loop is the center, not the whole. Every request passes through the same shape: load state → fit context → loop with the model → guard the tools → stream out → save state. When a framework's docs mention any of these words, you now know exactly where in that shape it sits.
So should you use a framework or roll your own? Now you can actually decide, because you understand the underlying problem. A framework saves you from rebuilding all that plumbing — but adds its own concepts to learn and constraints to live with. A hand-rolled loop is simple and fully yours — until you need streaming, retries, and hooks, and slowly rebuild a worse framework. Here are the concrete signals, both directions:
agent.py already covers it. Don't import a framework to do what one loop does.The point of building it by hand first. You spent this course doing the harness work manually on purpose. That's why you can now read any framework's feature list and see exactly what it gives you — and tell when its magic is worth the lock-in and when a 50-line loop is plenty.
For the finale: put your hand-rolled agent next to a real harness, see what they do that you don't, and add one missing piece to your own loop.
Two references so you can sanity-check what your assistant produces. First, the matrix — here's what two correctly filled-in rows of harness.md look like (yours will have all seven rows and a Strands column too):
| Feature | my loop (agent.py) | LangGraph | Claude Code |
|------------------|--------------------------------|-----------------|----------------|
| tool dispatch | ✓ hand-written run_tool() | ✓ built in | ✓ built in |
| retries/backoff | ✗ one failed call crashes it | ✓ configurable | ✓ automatic |
| streaming | ✗ silence, then the full answer| ✓ | ✓ word by word |
Second, the code change. If your assistant adds streaming, the diff to your Phase 4 loop should be surprisingly small — one call swapped for a streaming version. You don't write this; you just check that what your assistant wrote has this shape:
# BEFORE (Phase 4): silence, then the whole reply at once
reply = client.messages.create(
model="claude-sonnet-4-6", max_tokens=1024,
tools=TOOLS, messages=messages)
# AFTER: same call, streamed — words print as the model makes them
with client.messages.stream(
model="claude-sonnet-4-6", max_tokens=1024,
tools=TOOLS, messages=messages) as stream:
for text in stream.text_stream:
print(text, end="", flush=True) # <- this is "streaming"
reply = stream.get_final_message() # the loop continues as before
Notice what didn't change: the loop, the tools, the cap. Streaming wraps around the same call — one more piece of harness, zero change to the model. (If your assistant adds a hook instead, look for the same smallness: your guardrail function called right before or after run_tool.)
Deliverable — harness.md. A feature comparison of the harnesses you've used, plus one new feature added to your own loop. You can now look at any agent framework and know exactly what it's doing for you — because you've built, by hand, almost everything it sells.