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.
Why shallow agents fail on long tasks advanced
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
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.
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
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.
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
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
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.
Putting the four pillars together expert
A deep agent is a LangGraph graph that wires all four into one loop:
| 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"?