This is a survey — small examples of each pattern, no deep dives. The goal is vocabulary and shape recognition, so you can navigate the space when you build things later.
Patterns we'll cover
The agent loop as the foundation
What Claude Code actually is, architecturally
Subagents — recursion for agents
Memory scopes — what persists, where
Multi-model routing
Prompt caching for static prefixes
Long-running & async tasks
Computer use (vision agents)
Safety / permissions in agents
Vocabulary
Agent, agentic loop, harness
Subagent, Task tool, context isolation
Tool registry, dispatcher
Permission gate, hook
Computer use, vision tool
Router, fallback, ensemble
cache_control: ephemeral
Lecture
The agent loop — the engine
Every "agentic" system you've heard of has this loop at its core. The differences are what's wrapped around it.
def agent(messages, tools):
while True:
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system="...",
messages=messages,
tools=tools,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason == "end_turn":
return resp # we're done
results = []
for block in resp.content:
if block.type == "tool_use":
out = dispatch(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": out,
})
messages.append({"role": "user", "content": results})
~20 lines. Everything else in this module is a layer that sits above this loop or wires more tools into it.
Lecture
What is Claude Code, actually?
Claude Code (and tools like it — Cursor, Aider, Cline, Zed agent) are the agent loop above plus a thick stack of supporting machinery.
User interface
CLI · IDE extensions · slash commands · plan mode · status display
"Claude Code is a tool-use loop with a thick harness." The same recipe applies to building your own agentic app — you don't need to invent it from scratch.
Lecture
Subagents — recursion for agents
A subagent is just another Claude conversation, started by the parent, with its own context window, tool set, and system prompt. The parent gets back a single summary.
# Inside the parent agent's loop, the model decides to call the Task tool:
{
"type": "tool_use",
"name": "Task",
"input": {
"description": "Find all HTTP route definitions",
"subagent_type": "explore",
"prompt": "Search the codebase for Flask/FastAPI/Express route decorators.
Return a list of (file, line, route) tuples. Don't summarise the
implementations — just the routes."
}
}
# The harness runs a brand-new Claude session with that prompt + the
# Explore agent's tools. Parent receives only the final report.
Why use them
· Context isolation — the parent doesn't see the 40 files the explorer read
· Specialization — each subagent type has a tuned system prompt + restricted tools
· Parallelism — multiple subagents can run concurrently
· Recursion — subagents can call subagents (use with care; budgets matter)
Lecture
Memory — what persists, where
"Memory" in LLM apps isn't one thing — it's several different scopes with different lifecycles, storage, and retrieval strategies.
Scope
Lifetime
Stored where
Example
Conversation
Current session
In-process / Redis
The message array on every turn
Session
Until user logs out
Server-side store
Current open files, selected scope
User
Across sessions
DB or files keyed by user
"Larry prefers terse answers"; saved preferences
Project
While project exists
Repo (e.g., CLAUDE.md)
Coding conventions, file map, key paths
World
Indefinite
Vector store / RAG
Indexed documents, web data, knowledge bases
A serious agent uses several at once. Claude Code reads CLAUDE.md (project), ~/.claude/memory/*.md (user), and the running session (conversation) on every turn — three scopes, one composite prompt.
Lecture
Multi-model routing
Use a cheap model to decide what to do; an expensive model to do it. Bound cost without giving up quality on the calls that matter.
def route(task: str) -> str:
# 1. Cheap classifier — Haiku, no thinking, low effort
kind = call(
model="claude-haiku-4-5",
system="Classify the task as trivial / reasoning / general.",
user=task,
schema=TaskKind,
thinking=False, effort="low",
)
# 2. Route the actual work to the right model
if kind == "trivial":
return call(model="claude-haiku-4-5", user=task)
if kind == "reasoning":
return call(model="claude-opus-4-7", user=task, effort="high")
return call(model="claude-sonnet-4-6", user=task)
When this pays:
High volume + variable difficulty (lots of cheap calls, occasional expensive ones)
Latency-sensitive paths (route obvious things to fast model)
Cost ceilings (cap the % of calls that hit the expensive model)
"Route quality where it matters; route speed where it doesn't." Also works with fallbacks (primary fails → try secondary) and ensembles (run two, pick best).
Lecture
Prompt caching — pay once for the static prefix
Long, static system prompts (rules, schemas, examples, retrieved context) get expensive when called often. Anthropic's prompt cache marks a prefix as cacheable; repeated calls with the same prefix read from cache at ~10% the cost.
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
system=[{
"type": "text",
"text": LONG_SYSTEM_PROMPT, # 4KB+
"cache_control": {"type": "ephemeral"}, # cache it
}],
messages=[{"role": "user", "content": user_query}],
)
# First call:
# usage.cache_creation_input_tokens = 8200
# usage.cache_read_input_tokens = 0
# Next call within 5 min, same system prefix:
# usage.cache_creation_input_tokens = 0
# usage.cache_read_input_tokens = 8200 # ~90% cheaper
When it pays
Long static system prompts repeated across many calls (rules + examples + schemas)
RAG: cache the retrieved context for follow-up questions on the same chunks
Agentic loops: each turn re-sends the same tool definitions — cache them
Min cacheable prefix ~4096 tokens. Cache lives 5 minutes (sliding window — every read extends it). Static rules + schemas go in the cached prefix; the dynamic question goes in messages. Pairs naturally with multi-model routing — cache the part that doesn't change, route what does.
Lecture
Long-running & async tasks
Sometimes the work doesn't fit in one HTTP request. The agent needs an out-of-band way to track progress.
Patterns
Background jobs — submit, get job_id, poll for status
Subprocess monitoring — start a long shell process; agent watches stdout
Scheduled wake-ups — agent says "wake me in N min"; server reschedules
Webhooks / callbacks — external event resumes the agent
# Agent kicks off work it can't wait on
job_id = submit_task(
prompt="Analyse this 2GB log file",
callback_url="https://my.app/agent/resume",
)
return f"Started job {job_id}, will resume when done."
# Later, external worker finishes and POSTs back:
@app.route("/agent/resume", methods=["POST"])
def resume():
result = request.json["result"]
continue_agent_with(result)
Stateful agents need a way to pause and resume. The architecture choice is: where does the state live while the agent is paused?
Lecture
Progress messages — don't go silent
A long operation that prints nothing reads as hung. Have the worker emit progress events as it goes, and stream them to the user. This is exactly what makes Claude Code feel alive — "Reading 12 files…", "Running tests…".
Principles
Emit early and often — one event per meaningful step, not just at the end
Human-readable — say what's happening ("Calling search_docs…"), not just a spinner
Decouple producer from transport — the worker yields events; the caller decides how to surface them (SSE, websocket, log)
Always send a terminal event — a done or error so the client knows to stop waiting
Caller — stream each event (SSE)
@app.route("/agent")
def agent():
def stream():
for ev in run_agent(request.args["q"], TOOLS):
yield f"data: {json.dumps(asdict(ev))}\n\n"
return Response(stream(),
mimetype="text/event-stream")
from dataclasses import dataclass, asdict
from typing import Iterator
@dataclass
class Progress:
step: str # shown to the user
done: bool = False
def run_agent(user_msg, tools) -> Iterator[Progress]:
messages = [{"role": "user", "content": user_msg}]
while True:
resp = client.messages.create(
model="claude-sonnet-4-6", max_tokens=1024,
tools=tools, messages=messages,
)
messages.append({"role": "assistant",
"content": resp.content})
calls = [b for b in resp.content
if b.type == "tool_use"]
if not calls: # model is done
yield Progress("Done", done=True)
return
results = []
for call in calls:
yield Progress(f"Calling {call.name}…") # ← before the slow part
output = dispatch(call.name, call.input)
results.append({"type": "tool_result",
"tool_use_id": call.id,
"content": output})
messages.append({"role": "user", "content": results})
Emitting before each tool call is what lets the user watch the agent's reasoning unfold. The same generator works for any long operation — an indexing job, a batch run, a shell process — and never knows whether it's feeding a CLI, a web UI, or a log file. That's the point.
Lecture · Computer use (1 of 2)
The computer tool — the schema
It's a built-in (server-defined) tool. You don't write its input_schema — you pick a versioned type and pass your screen size. The model then emits action calls.
Claude never touches your machine. It only decides. Your harness owns the environment, captures the screen, and performs each action.
Environment
Container / VM
Linux desktop on a virtual display, with the target app running. Your app launches & owns it.
① screenshot (PNG)
→——————→
←——————←
④ execute (xdotool)
Your app / harness
The orchestrator
Runs the loop: grab screenshot → call the API → receive an action → perform it on the environment.
② image + messages
→——————→
←——————←
③ tool_use: action
Anthropic API
Claude (vision)
Sees the screenshot, decides the next action, returns a tool_use block. Never executes it.
↻ repeat every turn until Claude stops calling the tool
while not done:
img = capture(vm) # ① you
resp = client.messages.create( # ②
tools=[computer_tool],
messages=msgs + [user_image(img)])
for b in resp.content: # ③ Claudeif b.type == "tool_use":
perform(vm, b.input) # ④ you
Reference container: Anthropic ships a Docker image (Linux + a Python tool wrapping xdotool) so you don't build steps ① & ④ from scratch.
Costs: slow (a screenshot per turn), vision-token heavy, brittle to UI change. If an API or accessibility tree exists, use that instead.
Lecture
Safety in agents — permissions & limits
Agents that can act need different safety thinking than chatbots that can only talk. Two patterns dominate.
Permission gates
Each tool call is mediated. Categories of policy:
Always allow — read-only ops, safe by definition
Confirm — destructive ops (file delete, git push, API mutation)
Sandbox — bash inside a container, network restricted
Never — hard-coded refusals (delete prod, exfiltrate keys)
Claude Code, Cursor, Cline all expose these as user-tunable policies.
Hard limits
Step budget — max iterations of the loop
Token budget — kill the agent if it crosses N tokens
Time budget — wall-clock deadline
Tool-call rate — circuit-break on repeated identical calls (loop detection)
Scope — limit which files / endpoints / hosts the agent can touch
Agents fail interestingly. The risk isn't usually one disastrous tool call — it's 10,000 small ones that quietly burn budget, fill logs, or thrash on a bad assumption. Hard limits catch the latter.
Cheap-decides + expensive-executes. Bound cost without losing quality.
Prompt caching
Cache the static prefix. ~90% cheaper on repeat calls.
Long-running
Background, callbacks, batch. Where does state live while you wait?
Safety
Permission gates + hard limits. Catch the 10,000 small failures.
You can build a capable agent with everything above. But shipping it to real users takes more than architecture — you have to know it works and keep it safe. That's next.
Lecture
The agentic loop, drawn
The same ~20 lines as a picture: call the model, branch on stop_reason, run the tools, feed results back, repeat.
Subagents, memory, routing, and caching are all layers wrapped around this loop.
Architecture & Agents
Project
Build a data-analyst agent
Project
A data-analyst agent
The agent loop from this module, wired to three read-only tools over a SQLite database. It runs as an interactive, multi-turn session — like this prompt — so follow-ups keep the context.
you › Top product by revenue last quarter?
→ list_tables() → describe_table(order_items) → run_query(SELECT …)
Robot Vacuum — $46,307 (most recent quarter, completed orders).
you › What about the quarter before that? # ← follow-up uses the history
→ run_query(SELECT …)
Robot Vacuum again — $29,637, so revenue jumped ~56%.
What's in the zip
agent.py — the ~20-line loop + a CLI
tools.py — list_tables, describe_table, run_query
build_db.py + data.db — a seeded store (~3k orders, 18 months)
Your job (pick 2)
Add a tool — e.g. monthly_trend or save_report; register it in TOOLS + dispatch.
Probe the limits — find a question it gets wrong or answers inefficiently; show the transcript.
Tighten the loop — stream progress, add loop detection, or surface the final SQL.
Swap the data — point build_db.py at a different schema; the agent should work unchanged.
Spot the two safety patterns from this module already in place: read-only by construction (the query tool opens the DB in mode=ro, so SQLite rejects any write) and a hard step budget (MAX_STEPS) so a confused agent can't loop forever.
Beyond this project
Same agent, as a Claude Managed Agent
Our project is a tool-use loop you own. Managed Agents (beta) flips it: Anthropic runs the loop and hosts the workspace; you define a persisted agent and react to an event stream.
This project — Messages API + tool use
Managed Agents
The loop
You write the ~20-line while loop
Anthropic's orchestration layer runs it
Multi-turn state
Your messages list
The session — stateful server-side (free)
Tools run
On your machine (tools.py)
In a hosted container — or host-side as custom tools
Config
Inline in the script
A persisted, versioned Agent + a Session per run
You consume
A function's return value
An SSE event stream
"The agent loop is ~20 lines; everything else is harness." Managed Agents is Anthropic shipping the harness. For a self-contained CLI, the loop you own is the right call — reach for Managed Agents when you want hosted execution, versioned configs, file mounts, or server-managed sessions.
Beyond this project
The same tools — Anthropic runs the loop
Keep tools.py exactly as-is (read-only run_query and all). Declare the three tools as custom, and our dispatch() moves from a while loop into an event handler — the DB never leaves your machine.
from tools import dispatch # the SAME read-only tools.py, unchanged# ── once: persist a versioned agent (model/system/tools live here, not on the session) ──
agent = client.beta.agents.create(
name="Data Analyst", model="claude-sonnet-4-6",
system="Inspect the schema before querying…",
tools=CUSTOM_TOOLS, # list_tables / describe_table / run_query, each type:"custom"
)
# ── per run: no while-loop — react to the event stream ──
session = client.beta.sessions.create(agent=agent.id, environment_id=env.id)
with client.beta.sessions.events.stream(session_id=session.id) as stream:
client.beta.sessions.events.send(session_id=session.id, events=[
{"type": "user.message", "content": [{"type": "text", "text": question}]}])
for event in stream:
if event.type == "agent.custom_tool_use":
out = dispatch(event.name, event.input) # ← our tools, our machine, our mode=ro gate
client.beta.sessions.events.send(session_id=session.id, events=[
{"type": "user.custom_tool_result",
"custom_tool_use_id": event.id,
"content": [{"type": "text", "text": out}]}])
elif event.type == "session.status_idle" and event.stop_reason.type != "requires_action":
break
Multi-turn is now free — the session is stateful server-side, so the messages list we hand-maintained disappears. (Alternatively: mount data.db as a file and let the built-in toolset query it inside Anthropic's sandbox — where the read-only file mount replaces our mode=ro gate.)
Up next
Production Considerations
Eval & testing, guardrails & security — making it trustworthy