PHASE 12 · HARNESS

The runtime around the model

Goal: Understand what production agent frameworks actually do for you.

What this is

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?

The plan

  1. Make a feature comparison matrix: tool dispatch, retries, streaming, hooks, permissions, session persistence, observability. Fill in: your hand-rolled loop, LangGraph, Strands, Claude Code.
  2. Pick one feature your loop is missing (most commonly: streaming, or pre/post-tool hooks). Add it.
  3. Optional: build a minimal harness — agent loop with pluggable hooks (before_tool, after_tool, on_error), a permission system, and session state.
  4. Write a short doc: when to use a heavy framework vs. when to build your own. What does Claude Code's harness give you that you'd otherwise reinvent?

Ask your coding assistant

  • What does an agent 'harness' actually do? Make me a checklist.
  • Claude Code architecture deep-dives — what's in the harness?
  • LangGraph internals — how does its state management work?
  • What permission models are used in agentic systems? (allow-all, prompt-per-action, scoped, etc.)
  • When should you build your own harness vs. adopt a framework?

What you'll have when you're done

  • You understand the production complexity that lives outside the model.
  • You can evaluate any new agent framework in 30 minutes — you know what to look for.
  • You can decide build-vs-buy on agent infra with clarity.
  • You see Claude Code (and similar tools) as a harness running a model — demystified.

Deliverable

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

Intuition

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.

What a harness does

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:

  • Tool dispatch — routing tool calls to your real functions and feeding results back. This is your Phase 4 loop.
  • Retries & backoff — network calls fail; a harness retries them (waiting a bit longer each time — that's the "backoff") so your agent doesn't crash.
  • Context management — trimming and compacting so you never blow the window. Your Phase 8 compaction work.
  • Streaming — showing the answer word by word as it's generated, instead of a long silence and then a wall of text.
  • Hooks — run your code before or after each tool call, or when something errors. This is exactly where your Phase 7 guardrails attach: an input check as a before-hook, a PII redactor as an after-hook.
  • Permissions — pause and ask the human before a risky tool runs. Remember Phase 4's warning that the model can't tell arithmetic from sending a real email? Permissions are the harness acting on that warning.
  • Session state — save where a conversation left off and load it back next time. Your Phase 8 memory, productionized. (Add Phase 6's tracing and you have observability too.)

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.

Build vs buy

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:

Build your own loop when…

  • You need full control of the loop — custom stopping rules, an unusual order of steps, logic a framework would fight you on.
  • Your tools are unusual — they don't fit the standard "function with a description" shape (long-running jobs, hardware, weird approval flows).
  • You need tight cost control — you want to see, count, and trim every token yourself, the way you did in Phases 6 and 8.
  • The whole job fits in ~50 lines — your agent.py already covers it. Don't import a framework to do what one loop does.

Adopt a harness when…

  • It's a standard chat or coding-assistant use case — the well-worn path every framework was built and battle-tested for.
  • You want streaming, permissions, and session persistence for free — exactly the three components you never built by hand. That was deliberate: they're fiddly, solved problems.
  • You'd rather ship the product than maintain plumbing — retries, backoff, and reconnects are not where your app wins or loses.
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.

Compare & extend

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.