Deep Agents — Reflection, Planning & Long-Term Memory
A shallow agent does one ReAct loop and stops. A deep agent tackles long, open-ended work by combining four capabilities: an explicit plan, reflection to self-correct, long-term memory that survives the context window, and sub-agents for focused subtasks. This is the pattern behind "deep research" and coding agents — and the synthesis of everything in this module.
Learning objectives
- Explain what makes an agent "deep" — the four pillars and why long tasks need them.
- Give an agent an explicit, updatable plan (a to-do list as state).
- Add a reflection/critique step that improves output before finalizing.
- Add long-term memory that persists beyond one run and beyond the context window.
- Use sub-agents to keep the main context clean, and assemble all four into one agent.
30-second recap
- Agent & tool foundations: The model asks to call a tool, your code runs it and returns the result, and the loop repeats until the model answers — with step caps so it can't run forever.
- Agent architectures: Agents differ in who controls the flow: fixed workflows, a single tool-using loop, or graphs of steps with explicit state.
Why shallow agents fail on long tasks advanced
Picture handing a bright but forgetful intern a week-long project with no notebook, no checklist, and a memory that wipes every few hours. They'd start strong, do good work for an afternoon, then slowly lose the plot — re-deriving decisions they already made, forgetting the third of five requirements, and turning in whatever they happened to be holding when the clock ran out. That's not a talent problem; it's a process problem. A shallow ReAct agent has exactly this shape, and no amount of "just be smarter" in the prompt fixes it.
Here's the honest thing most tutorials skip: the plain loop isn't broken — it's simply operating past the horizon it was built for. A ReAct agent thinks, acts, observes and repeats, holding everything it knows in one growing context window. For a three-step task that's perfect. But "research this market and write a 20-page report" isn't three steps; it's dozens of sub-tasks stretched over hundreds of tool calls, and every one of them competes for the same finite window. The failures below aren't random bugs — they're the predictable consequences of asking one flat loop to carry a load it was never sized for.
The common mistake is to attack these symptoms with prompt tweaks — "remember your earlier findings," "double-check your work," "don't lose track of the goal." It feels like it should work, and it fails for a subtle reason: you're asking the model to do reliably, from inside a filling context, the very bookkeeping the context is too small to hold. The deep-agent answer is the opposite move — take that bookkeeping out of the model's head and into the system around it: a plan it re-reads, a critic that runs with fresh eyes, a store that outlives the window, and helpers that shoulder their own context. A plain ReAct agent (L1) works great for a few steps. Point it at "research this market and write a 20-page report" and it falls apart in predictable ways — the exact failures the four pillars fix.
| Shallow-agent failure | Deep-agent fix |
|---|---|
| Loses the thread over many steps | Planning — an explicit to-do it keeps returning to |
| Ships the first draft, mistakes and all | Reflection — critique & revise before finalizing |
| Forgets earlier findings as context fills | Long-term memory — offload to durable storage |
| One context tries to hold everything | Sub-agents — isolate subtasks, return only results |
This picture is the whole lesson in one glance. A basic "agent" is just a loop: think, use a tool, look at the result, repeat. That loop is the box in the middle. A deep agent wraps four extra abilities around that same loop so it can handle long, messy tasks without falling apart.
- The centre box,
agent loop, is the ordinary think-act-observe cycle you already know. Everything else on the diagram is bolted onto it. - Reading clockwise from the top: 1 · Plan (a written to-do list so the agent knows the next step), 4 · Sub-agents on the right (hand a focused chunk of work to a helper), 3 · Memory at the bottom (save findings somewhere durable), and 2 · Reflect on the left (check its own work before finishing).
- The dashed arrows point inward from each pillar to the loop — meaning each ability feeds into and supports that central cycle; the loop is not replaced, it's strengthened.
- The line under the picture is the one-sentence summary: a core loop wrapped in plan + reflect + memory + delegation. If any single pillar is missing, long tasks start to degrade in the way the table above describes.
In short: "Deep" is not a new tool or library — it's these four helpers added around the plain loop. The rest of the lesson builds them one at a time, then snaps them together.
Lab M4.1 · Planning as state advanced
Think about how you tackle a big move. You don't hold "pack the kitchen, cancel the internet, book the van, forward the mail" purely in your head and hope — you write it down, then work the list, crossing off items and adding the ones you forgot. The written list is doing something your memory can't: it's an external, re-readable record of intent that survives distraction. That is exactly what planning-as-state gives an agent, and it's why this is the first and highest-leverage pillar — the cheapest change with the biggest effect on a long run.
Why store the plan as data rather than just prompting for it? Because a plan that lives only in the model's reasoning is written in disappearing ink. As the context fills with tool results, that reasoning scrolls out of view, and the agent quietly substitutes a fuzzy sense of "roughly what I'm doing" for the crisp list it started with. Put the plan in a state field — an ordinary list of steps the code owns — and it stops being a memory and becomes a source of truth: the agent can re-read it at the top of every step, mark items done, and re-plan when reality diverges from expectation. The model does the thinking; the state holds the truth. That division of labour is the whole trick.
The common mistake is treating the plan as write-once — generate a nice numbered list at the start, then never look at it again. A stale plan is almost worse than none, because it lulls you into thinking the agent is on track while it improvises. A good plan is a living field: read it, act on the top open item, record the outcome, and if the task turned out different than expected, rewrite the remaining steps. The tiny loop below has exactly that spine — one node writes the plan, another checks whether any steps remain — and everything richer (sub-tasks, re-planning) is just more logic hung on this skeleton. The simplest, highest-leverage pillar: give the agent an explicit to-do list in its state that it writes first and updates as it goes. It's L1's plan-and-execute, made durable via L4 state.
Read left-to-right, then follow the loop back: the stored list on the left feeds one step at a time into the execute box, and the outcome loops back to update the list.
- The left column of amber boxes is the plan held in a
statefield — an ordinary list of steps (research X,draft,review) the code can read and rewrite. - The blue arrow carries the top open item into the
execute stepbox — the agent always acts on the next unfinished step, not on a hazy recollection. - The green box marks that item done and re-plans; the dashed arrow loops back to the list so the next open item can be read. The list, not the model's head, holds the truth.
In short: Write the list into state first, then read → act → mark done → re-plan on repeat until no open items remain.
Requires: pip install langgraph
plan.pyfrom typing import TypedDict, Annotated
from langgraph.graph import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
plan: list[str] # the to-do list, e.g. ["research X", "draft", "review"]
done: list[str] # completed steps, for progress & self-check
def make_plan(s):
steps = model.invoke(f"Break this task into 3-6 concrete steps:\n{s['messages'][-1]}")
return {"plan": _parse_steps(steps), "done": []}
def next_step(s) -> str:
return END if not s["plan"] else "execute" # plan empty → finished
This is the first and highest-value pillar: give the agent a written plan it stores as data and keeps returning to. Instead of the plan living invisibly "in the model's head," it lives in a shared state object the code can read, update, and check — so the agent can't quietly lose the thread on a long job.
Statedescribes the shared scratchpad every step reads and writes.messagesis the running conversation;planis the list of to-do steps (e.g.["research X", "draft", "review"]);doneis the list of steps already finished, so you can see progress.make_planruns first: it asks the model to "Break this task into 3-6 concrete steps" based on the user's latest message, then stores those steps intoplanand startsdoneempty. This is the "write a to-do list first" habit, in code.next_stepis the traffic controller: if the plan list is empty (not s["plan"]is true when there's nothing left) it returnsENDto stop; otherwise it returns"execute"to go do the next step. That's how the agent loops until the whole plan is finished.
What the output means: Nothing prints here — this is the wiring. In a full run, make_plan would fill plan with a handful of steps, and the agent would work through them one by one until next_step sees an empty list and returns END.
Try this: On paper, start with plan = ["a", "b"] and imagine removing one step each pass. next_step returns "execute" twice, then END — that is the entire plan-driven loop.
Lab M4.2 · Reflection advanced
Every writer knows the trap: the draft you just wrote always reads better to you than it is. You're too close to it — you see what you meant, not what's on the page, so the missing caveat and the half-answered question slide right past. The fix professionals use isn't "try harder"; it's to put the work down and come back with fresh eyes, or better yet hand it to an editor who wasn't in your head while you wrote. Reflection is that editor, built into the agent: a second pass that critiques the draft against the requirements before anyone calls it finished.
Why this needs to be a distinct step, and why a fresh critic matters. If you simply ask the same reasoning that produced the draft "is this good?", you tend to get a rationalization — the model defends the choices it just made, because they're right there in its context looking justified. The stronger move is to run the critique with a separate prompt and a clean framing ("list concrete problems, or reply PASS"), so it's genuinely evaluating the artifact rather than protecting its author. That's the same generate-then-critique split you met in L1, now expressed as a bounded loop: critique, and if there are real problems, feed them back to the generator and try again.
The common mistake — and it's the one that bites hardest — is leaving the loop unbounded. "Keep revising until it's perfect" sounds diligent and is actually a token bonfire: past a pass or two the critic runs out of substantive faults and starts nitpicking, or worse, second-guesses good decisions and makes the draft worse. Reflection has a genuine sweet spot, and you enforce it with a hard cap (two revisions is a sound default) plus an explicit PASS exit. The snippet below shows exactly that shape — a critique that can say PASS, a revision counter that forces a stop, and a graph edge that routes "revise" back to the generator and "finish" to the end. Before finalizing, a deep agent critiques its own work and revises — L1's reflection architecture, as a bounded loop (L5). A fresh-context critic catches what the generator missed.
Follow the arrows clockwise: a draft goes to the critic, which either passes it through to finish or sends problems back down to the generator.
- The left blue box generates a draft and hands it to the centre critic box — deliberately a fresh prompt, so it grades the artifact rather than protecting the reasoning that produced it.
- The green arrow is the exit: if the critique says
PASSor the revision cap is hit, the draft goes tofinish. - The amber loop-back arrow is the revise path — concrete problems return to the generator for another pass, but only up to two times, which is the whole point: reflection has a sweet spot and the cap enforces it.
In short: Generate → critique with fresh eyes → PASS or bounded revise → finish; the hard cap is what stops it burning tokens forever.
Setup to run this snippet
END = "__end__" # sentinel used by graph examples
class _g_t:
add_conditional_edges = 'demo'
def add_conditional_edges(self, *a, **k): return 'demo'
def __getattr__(self, k): return 'demo'
g = _g_t()
class _model_t:
invoke = 'demo'
def invoke(self, *a, **k): return 'demo'
def __getattr__(self, k): return 'demo'
model = _model_t()reflect.pydef reflect(s) -> str:
"""Critique the draft against the plan; decide revise or finish."""
critique = model.invoke(
f"Critique this draft against the requirements. "
f"List concrete problems, or reply 'PASS'.\n\n{s['draft']}").content
if "PASS" in critique or s["revisions"] >= 2: # bound it — quality has diminishing returns
return "finish"
return "revise" # feed the critique back to the generator
g.add_conditional_edges("reflect", reflect, {"revise": "generate", "finish": END})
The second pillar is reflection: before the agent calls its work "done," it critiques its own draft and decides whether to revise. Crucially it uses a fresh critic prompt, so it reviews the draft with clear eyes instead of defending what it just wrote.
reflectasks the model to "Critique this draft against the requirements" and either list concrete problems or replyPASS. The critique text comes back in.content.- The
ifdecides when to stop: if the critique contains"PASS"(the draft is good enough) or the agent has already revised twice (s["revisions"] >= 2), it returns"finish". Capping revisions matters — extra passes give shrinking returns and can even undo good work. - Otherwise it returns
"revise", which sends the critique back so the generator can improve the draft and try again. - The last line,
g.add_conditional_edges(...), wires that decision into the graph: from the"reflect"step,"revise"loops back to"generate"and"finish"goes toEND.
What the output means: Not run directly. In practice the draft cycles generate → reflect → (revise → generate) at most twice, then the agent finishes with a cleaner result than its first draft.
Try this: Picture a draft that's missing one requirement. Pass 1 finds it and returns "revise"; the rewrite fixes it; pass 2 replies PASS → "finish". That's the bounded loop the warning box is describing.
Lab M4.3 · Long-term memory expert
Your working memory holds about seven things; your notebook holds everything. Nobody solves a hard problem by trying to keep every fact suspended in their head at once — you jot the important findings down and free your mind for the next step, trusting you can look the notes back up when you need them. An agent's context window is its working memory, and it's just as finite: on a long task the earliest, often most important, findings get shoved out to make room for the latest tool output. The model didn't decide those facts were unimportant — the window simply ran out of room. Long-term memory is the notebook that fixes this.
The key distinction is short-term versus long-term, and beginners routinely conflate them. Short-term memory is the messages currently in context (L2/L4) — fast, automatic, and doomed to be overwritten. Long-term memory is an external store — a file, a database, or a vector store — that the agent deliberately writes to and later recalls from as a tool. The crucial property is durability: what you save there survives not just the filling window but the end of the run entirely, so a research agent can pick up tomorrow where it left off today. Mechanically it reuses machinery you already have: a durable backend (L5's checkpointer or a real DB) plus semantic recall (M3's retrieval) to fetch the right note by meaning rather than exact wording.
The common mistake is treating memory as a dumping ground — saving whole raw transcripts, every tool result, the full conversation "just in case." That poisons the well: recall then returns pages of noise, costs more tokens, and buries the one fact you actually needed. The real skill here isn't the plumbing (the lab below is only a few lines) — it's the judgement about what deserves to be remembered. Save distilled decisions and findings — "the user prefers concise answers," "source X contradicts the client's claim" — not the transcript that produced them. A good memory is curated, not comprehensive. The context window is finite; a long task overflows it. Long-term memory offloads durable facts to external storage the agent reads and writes as a tool — surviving both the context window and the run itself.
This diagram explains the third pillar, long-term memory. The problem it solves: the model's working memory (its "context window") is a fixed size, so on a long task the earliest findings get pushed out and forgotten. The fix is to keep important facts in a separate store outside the context.
- The left box,
agent (context), is the agent's short-term working memory — only the messages currently in the conversation. It's limited and temporary. - The top arrow labelled write findings goes from the agent out to the store: when the agent learns something worth keeping, it saves it.
- The lower dashed arrow labelled recall when needed goes back the other way: later, the agent asks the store to hand a saved fact back into context.
- The right box,
memory store, is marked durable · outlives context — a file, database, or vector store that survives even after the run ends. The caption sums up the split: short-term = messages in context; long-term = this external, persistent store.
In short: Think of the context window as your desk (small, cleared each day) and the memory store as a filing cabinet (keeps things for good). The agent writes notes to the cabinet and pulls them back out when needed.
Requires: pip install langchain-core
memory.pyfrom langchain_core.tools import tool
@tool
def save_memory(key: str, value: str) -> str:
"""Save a durable fact for later. Use for findings you'll need after the context fills."""
store.put(key, value) # file / DB / vector store — outlives this run
return "saved"
@tool
def recall_memory(query: str) -> str:
"""Retrieve durable facts saved earlier."""
return store.search(query) # semantic recall (M3 retrieval)
agent = create_react_agent(model, tools=[save_memory, recall_memory, ...])
Here long-term memory becomes real code. The trick: give the agent two tools — one to save a fact and one to look facts up. A "tool" is just a function the model is allowed to call on its own, so the agent decides when to remember and when to recall.
- The
@tooldecorator marks each function as something the agent can call. The text inside the triple-quoted"""docstring"""is not a comment for you — the model reads it to decide when to use the tool, so it's written as an instruction. save_memory(key, value)stores a fact under a name viastore.put(...). Its docstring tells the agent to use it for findings you'll need after the context fills — exactly the "write findings" arrow from the diagram.recall_memory(query)is the other direction:store.search(query)finds relevant saved facts by meaning (the same semantic search from the RAG lesson), matching the "recall when needed" arrow.- The last line,
create_react_agent(model, tools=[save_memory, recall_memory, ...]), hands both tools to the agent so it can save and recall on its own during a run.
What the output means: No output on its own — this defines the memory tools. Once handed to the agent, calling save_memory returns "saved" and recall_memory returns the stored text later, even after earlier messages have scrolled out of context.
Try this: The real skill isn't the code — it's judgement about what to save. Store distilled decisions and findings, not whole raw transcripts, so recall stays useful and cheap.
Sub-agents: focus through isolation expert
A good manager doesn't sit in on every phone call their team makes. They hand a report the brief — "check these three sources and tell me what you find" — and get back a one-paragraph summary, not a transcript of every dead end, wrong turn and re-read along the way. If the manager did absorb all that raw detail from five reports at once, their own head would be so cluttered they'd lose sight of the project. Delegation isn't just about doing more at once; it's about protecting the manager's attention. Sub-agents are that same instinct applied to context windows.
This is why the framing "context hygiene, not just parallelism" matters, and it's the point most people miss. The obvious reason to spawn sub-agents is speed — investigate three sources concurrently instead of one after another. Real, but secondary. The deeper win is isolation: a sub-agent researching one source burns through dozens of tool calls, false starts and half-relevant snippets inside its own context, then hands the orchestrator only the clean, distilled result. The parent's window never sees the mess. So the main agent's context stays focused on the plan and the findings that matter, instead of drowning in the intermediate noise of every subtask — which is exactly the failure mode a single flat loop hits. It's the supervisor topology from L1/M1, reused wholesale.
The common mistake is delegating for the sake of delegating. Spinning up a sub-agent for a trivial one-line step adds a whole extra LLM round-trip, its own failure surface, and coordination overhead — all to isolate context that was never going to overflow. Reach for a sub-agent when a subtask is both focused (a clean, self-contained brief) and context-heavy (it'll generate a lot of intermediate work you don't want in the parent). Verifying one claim across ten documents: yes. Formatting a date: no. The fourth pillar reuses M1/M2 directly. A deep agent spawns a sub-agent for a focused subtask (research one source, verify one claim); the sub-agent burns its own context and returns only the result, keeping the main agent's context clean and on-plan.
Read top-down for delegation, then bottom-up for the return: the orchestrator hands briefs down to isolated helpers, and only distilled results come back up.
- The top box is the orchestrator — the main agent that must stay focused on the plan. The blue arrows fanning down hand each sub-agent a focused brief.
- The three sub-agent boxes each have their own context. Their dozens of tool calls, dead ends and half-relevant snippets (the grey note) stay inside that box and never reach the parent.
- The green arrow back up carries only the clean, distilled result — that upward path is why the orchestrator's window stays uncluttered even as the total work grows.
In short: Delegate a focused, context-heavy subtask; let the mess live in the sub-agent; take back only the answer.
Putting the four pillars together expert
You've now built each pillar in isolation — but a deep agent is the composition, not the parts. The pieces reinforce each other in a loop: the plan decides what to do next; executing a step may spawn a sub-agent for the heavy lifting; whatever it learns gets written to long-term memory; the item is marked done and the plan re-checked; and before anything ships, reflection critiques the result. Round and round until the plan is drained. Nothing here is new — it's L1's plan-execute and reflection, L4's state, L5's persistence and cycles, and M1/M2's sub-agents, snapped together into one graph. "Deep agent" names the assembly, not a new framework.
The most important thing to internalize is the honest cost of this, because the skeleton makes it look free. Every pillar you add multiplies LLM calls, latency, and the surface where things can go wrong — a prompt injection in one retrieved source (T1, M3), a sub-agent that returns garbage, a reflection loop that runs long. The seasoned move is restraint: start with the shallow ReAct loop and add a pillar only when the task demonstrably fails without it. Most problems need one or two pillars, not all four; reaching for the full stack by default is over-engineering that costs real money and reliability. And notice the one non-negotiable in the skeleton below: no matter how sophisticated the agent grows, every irreversible action still passes the L5 human-in-the-loop safety gate. Depth buys capability; it never buys a pass on safety. A deep agent is a LangGraph graph that wires all four into one loop:
Follow the top row left-to-right, drop into the helpers below each step, then trace the loop back to the plan — with the red gate guarding any risky action.
- The top row is the spine: 1 · plan (amber) chooses the next step, execute step (centre) does it, 2 · reflect (green) critiques the result before finishing.
- Below the execute box, the dashed arrow shows a step may spin up a sub-agent (pillar 4), and the solid arrow shows it always writes to 3 · memory — the two helpers that support each step.
- The long dashed arrow loops from reflect back to plan: revise, or pick the next item, until the plan is drained. The red arrow routes every irreversible action through the L5 safety gate — depth never removes that check.
In short: plan → execute (delegate + remember) → reflect → loop, with a non-negotiable safety gate on anything irreversible.
| Pillar | Reuses | Buys |
|---|---|---|
| Planning | L1 plan-execute + L4 state | Coherence over long runs |
| Reflection | L1 reflection + L5 bounded cycle | Quality / self-correction |
| Long-term memory | L5 store + M3 retrieval | Endurance past the context window |
| Sub-agents | M1/M2 + L1 supervisor | Focus & clean context |
Common pitfalls expert
| Pitfall | Fix |
|---|---|
| Plan lives only in the model's head | Make it explicit state the agent re-reads and updates |
| Unbounded reflection loop | Cap revisions; use a separate critic context |
| Saving raw transcripts as "memory" | Save distilled findings/decisions, not noise |
| Sub-agents for trivial steps | Delegate only focused, context-heavy subtasks |
| Adding all four pillars by default | Start shallow; add a pillar only when it fails without it |
| Sophisticated agent, no safety gate | Depth doesn't replace the L5 HITL gate on risky actions |
Exercises expert
Exercise M4.1 — Plan-driven agent
Context: An agent that keeps its plan in state — writing it, executing it, and updating it — stays on a multi-step task instead of wandering. Watching it drain its own plan is the proof.
Your task: Build an agent that writes a plan into state, executes each step, and marks it done, looping until the plan is empty, then give it a multi-step task.
Requirements:
- The plan lives in state, written before execution begins
- Execute steps one at a time, marking each done as it completes
- Loop until the plan has no open steps left
- Confirm the agent follows and updates its own plan rather than wandering
💡 Hint: The signal of success is that the agent consults "what's next" from state each step, not that it happens to finish.
Exercise M4.2 — Add reflection
Context: A bounded reflection loop critiques a draft before finishing — but more reflection is not monotonically better. Pushing the cap up reveals the plateau where extra passes stop helping.
Your task: Add a bounded reflection loop (max 2 revisions) that critiques the draft before finishing, compare the reflected output to the first draft on a task with an easy-to-miss requirement, then push revisions to 5 and note where quality plateaus.
Requirements:
- Add a critique-and-revise loop capped at 2 revisions
- Choose a task with an easy-to-miss requirement so reflection has something to catch
- Compare the reflected output against the un-reflected first draft
- Raise the cap to 5 and observe where quality stops improving
💡 Hint: Revision 1→2 usually fixes real issues; past that the critic starts nitpicking or undoing good choices — that plateau is why you cap it.
Show what to look for
Revision 1→2 usually fixes real issues; past that the critic starts nitpicking or undoing good choices. That plateau is why you cap it — more reflection is not monotonically better.
Exercise M4.3 — Deep research agent (capstone of the module)
Context: The capstone combines all four pillars into a mini deep-research agent — the same shape as Project 6 in the gallery. By now every piece has been built in isolation; this wires them together.
Your task: Combine the four pillars into a mini "deep research" agent: plan the research, spawn sub-agents per source, save findings to memory, retrieve them to write the report, and reflect before finishing.
Requirements:
- Plan the research as explicit, updatable state
- Spawn a sub-agent per source, each with isolated context (from M1/M2)
- Save findings to long-term memory and retrieve them when writing the report
- Run a reflection pass before finishing
- Integrate all four pillars end to end, not as separate demos
💡 Hint: You already have every piece from the ladder and earlier modules; the deliverable is the integration, so lean on the seams you built rather than rewriting them.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: A shallow agent is a thin think-act loop that drifts on long tasks. Four pillars fix this — planning, reflection, long-term memory, and sub-agents — and each maps to a specific long-horizon failure it prevents.
Your task: Name the four pillars of a deep agent and map each one to the failure mode it prevents, as a runnable lookup.
Requirements:
- Enumerate all four pillars: planning, reflection, long-term memory, sub-agents
- Pair each pillar with the concrete failure it addresses
- Planning fixes drift; reflection fixes shipping a flawed first draft
- Memory fixes re-learning / lost context; sub-agents fix one overloaded context window
- Print the pillar-to-failure mapping
💡 Hint: The through-line: a long task needs an explicit plan to stay on target, reflection to self-correct, memory to persist, and sub-agents to keep each context focused.
Show solution
Each pillar targets a specific long-horizon failure. Runnable lookup:
PILLARS = {
"planning (to-do as state)": "drift / forgetting the goal mid-task",
"reflection (critique loop)": "shipping a first draft with obvious errors",
"long-term memory": "re-learning facts every run / losing context",
"sub-agents": "one context window overloaded with everything",
}
for pillar, fixes in PILLARS.items():
print(f"{pillar:<28} fixes -> {fixes}")
The through-line: a long task needs an explicit plan to stay on target, reflection to self-correct, memory to persist, and sub-agents to keep each context focused.
Context: "Make a to-do list first" is the single most effective long-horizon instruction, but only if the plan is durable state the agent re-reads each step rather than a line buried in the prompt.
Your task: Model a plan as updatable state: a Plan that creates tasks, marks them done, and always surfaces the next open task plus overall progress.
Requirements:
- Represent each task with a done/not-done flag
- Expose the next open task (skipping completed ones)
- Allow marking a specific task complete
- Report progress as a done/total count alongside the next task
- Demonstrate creating a plan and completing a few tasks
💡 Hint: Keeping the plan as inspectable state — not just in the prompt — is what stops a long-running agent from drifting; it re-reads "what's next" every step.
Show solution
The plan is durable state the agent reads and writes each step. Runnable:
class Plan:
def __init__(self, tasks):
self.tasks = [{"task": t, "done": False} for t in tasks]
def next_open(self):
for t in self.tasks:
if not t["done"]:
return t["task"]
return None
def complete(self, task):
for t in self.tasks:
if t["task"] == task:
t["done"] = True
def progress(self):
done = sum(t["done"] for t in self.tasks)
return f"{done}/{len(self.tasks)} done; next: {self.next_open()}"
p = Plan(["gather sources", "draft", "review", "publish"])
print(p.progress())
p.complete("gather sources"); p.complete("draft")
print(p.progress())
Keeping the plan as inspectable state (not just in the prompt) is what stops a long-running agent from drifting — it re-reads "what's next" every step.
Context: Reflection generates, critiques, and revises until the draft passes — but bounded, so it terminates. An unbounded critique loop is a classic way to burn tokens forever.
Your task: Model a draft → critique → revise loop that stops on a clean critique or after a fixed number of passes.
Requirements:
- A critique step returns a list of concrete problems with the draft
- A revise step fixes the reported problems and returns a new draft
- Loop until the critique is empty (PASS) or the pass cap is reached
- Bound the loop with a
max_passeslimit and report best-effort on exhaustion - Demonstrate a draft that needs several revisions to pass
💡 Hint: Reflection catches obvious errors before finalizing; the pass cap is essential, not optional — treat it as the termination guarantee.
Show solution
The bounded reflection loop from the lesson. Runnable:
def critique(draft):
problems = []
if len(draft.split()) < 5: problems.append("too short")
if "TODO" in draft: problems.append("has placeholder")
if not draft.endswith("."): problems.append("no closing punctuation")
return problems
def revise(draft, problems):
if "too short" in problems: draft += " with more detail added here"
if "has placeholder" in problems: draft = draft.replace("TODO", "the result")
if "no closing punctuation" in problems and not draft.endswith("."):
draft += "."
return draft
def reflect(draft, max_passes=3):
for p in range(1, max_passes + 1):
problems = critique(draft)
if not problems:
return f"PASS on pass {p}: {draft!r}"
draft = revise(draft, problems)
return f"stop after {max_passes} passes (best effort): {draft!r}"
print(reflect("TODO"))
Reflection catches obvious errors before finalizing; the max_passes bound is essential — an unbounded critique loop is a classic way to burn tokens forever.
Context: Long-term memory persists beyond a single run: durable facts are saved and later recalled by relevance. In production the recall is embedding-based, but the save/recall seam is identical to a simpler term-overlap stand-in.
Your task: Model a keyed Memory store with save(fact) and a recall(query, k) that returns the most relevant saved facts.
Requirements:
- Persist facts across calls in the store
- Recall ranks saved facts by relevance to the query (term overlap stands in for embeddings)
- Return only the top
kfacts, dropping zero-overlap ones - Note that production recall would be embedding-based (the M3 retriever)
- Demonstrate saving several facts and recalling by two different queries
💡 Hint: Memory turns a stateless agent into one that improves across sessions; keep the save/recall seam clean so the ranking can later be swapped for real embeddings.
Show solution
Persist facts and recall by term overlap (a stand-in for embedding search). Runnable:
class Memory:
def __init__(self):
self.facts = [] # would be a vector store in production
def save(self, fact):
self.facts.append(fact)
def recall(self, query, k=2):
qt = set(query.lower().split())
scored = [(len(qt & set(f.lower().split())), f) for f in self.facts]
scored = [s for s in scored if s[0] > 0]
return [f for _, f in sorted(scored, reverse=True)[:k]]
mem = Memory()
mem.save("The user prefers concise, bulleted answers.")
mem.save("The user's timezone is IST.")
mem.save("Project deadline is the 30th.")
print(mem.recall("what answer format does the user like"))
print(mem.recall("when is the deadline"))
Memory turns a stateless agent into one that improves across sessions; in production the recall is embedding-based (M3 retrieval), but the save/recall seam is identical.
Context: Sub-agents keep each context window focused: a manager delegates a subtask to a fresh agent that sees only the slice of context it needs, then merges the compact results. Isolation — not delegation alone — is the point.
Your task: Model a manager that delegates subtasks to isolated sub-agents, passing each only the context keys it needs, then merges their results.
Requirements:
- Each sub-agent receives only the context slice its subtask requires
- A sub-agent never sees the manager's full history or other agents' raw data
- The manager iterates the subtasks, delegating and collecting results
- Merge the sub-agents' outputs into a single result keyed by the goal
- Demonstrate a manager splitting a goal across at least two isolated sub-agents
💡 Hint: Isolation keeps every context window small, focused, and cheap — the writer should never see the researcher's raw sources, only what it needs.
Show solution
Each sub-agent gets a minimal, isolated context — not the manager's whole history. Runnable:
def sub_agent(name, task, context):
# isolated: only sees the slice it needs, returns a compact result
return {"agent": name, "task": task,
"result": f"[{name}] done '{task}' using {len(context)} ctx items"}
def manager(goal, subtasks, shared_ctx):
results = []
for name, task, needed_keys in subtasks:
isolated = {k: shared_ctx[k] for k in needed_keys if k in shared_ctx}
results.append(sub_agent(name, task, isolated))
return {"goal": goal, "merged": [r["result"] for r in results]}
ctx = {"sources": [1, 2, 3], "style": "concise", "budget": 100}
plan = [("researcher", "gather facts", ["sources"]),
("writer", "draft summary", ["style"])]
import json
print(json.dumps(manager("write a brief", plan, ctx), indent=2))
Isolation is the point: the writer never sees the researcher's raw sources, only what it needs — which keeps each context window small, focused, and cheap.
Context: A deep agent assembles all four pillars into one bounded orchestrator: plan the work, delegate each task to a sub-agent, reflect on its output, and persist learnings to memory. Offline you can model the full control flow; the SDK later adds durability and streaming.
Your task: Build an offline deep_agent(goal, tasks) that plans, delegates each task to a sub-agent, reflects on the output, and saves a learning to memory — then sketch the equivalent create_react_agent shape marked as needing the SDK.
Requirements:
- Turn the tasks into a plan with per-task done flags
- For each task: delegate to a sub-agent, run one bounded reflection pass, then persist a learning to memory
- Mark each task done and record a transcript of results
- Return the goal, a done flag, the step transcript, and the accumulated memory
- Include a commented
create_react_agent+ checkpointer sketch, labelled as needing the SDK - Note the SDK adds durable checkpointing, streaming, and real tool execution over this same orchestration
💡 Hint: The offline model is the orchestration logic of all four pillars; the SDK's checkpointer is what makes memory durable across runs.
Show solution
All four pillars in one bounded orchestrator. Runnable:
def deep_agent(goal, tasks):
plan = [{"task": t, "done": False} for t in tasks]
memory, transcript = [], []
for step in plan:
# 1) plan: take next task 2) sub-agent: do it (isolated)
raw = f"output for '{step['task']}'"
# 3) reflect: one bounded critique/revise pass
good = raw if raw.endswith("'") else raw + "."
# 4) memory: persist a learning
memory.append(f"learned: {step['task']} completed")
step["done"] = True
transcript.append({"task": step["task"], "result": good})
return {"goal": goal, "done": all(s["done"] for s in plan),
"steps": transcript, "memory": memory}
import json
print(json.dumps(deep_agent("write a report", ["research", "draft", "review"]),
indent=2))
# needs the SDK -- the same idea with LangGraph's prebuilt agent (documented API):
# from langgraph.prebuilt import create_react_agent
# from langgraph.checkpoint.memory import MemorySaver
# agent = create_react_agent(model, tools=[plan_tool, memory_tool, subagent_tool],
# checkpointer=MemorySaver()) # persistence = long-term memory
# agent.invoke({"messages": [("user", "write a report")]},
# config={"configurable": {"thread_id": "run-1"}})
The offline model shows the control flow of all four pillars; the SDK adds durable checkpointing (memory across runs), streaming, and real tool execution — but the orchestration logic is what you just modeled.
✓ Checkpoint — you can move on when you can…
- Name the four pillars and the shallow-agent failure each fixes.
- Give an agent an explicit, updatable plan in state.
- Add a bounded reflection loop with a separate critic.
- Add long-term memory that outlives the context window.
- Explain why sub-agents are about context hygiene, and assemble all four.
Knowledge check check yourself
What are the four pillars of a deep agent, and which shallow-agent failure does each fix?
Show answer
Why are sub-agents described as "context hygiene, not just parallelism"?