Module 11: Deep Agents
Evolving Project: The Deep Agent Version (v6)
Project overview
You've reached the final project of Module 11 and the most important teaching moment in the guide.
Across modules 6-10, your AI Research Assistant grew piece by piece: basic search with the Functional API (v1), retry and branching (v2), checkpointing and crash recovery (v3), HITL with approvals (v4), and multi-agent with a supervisor and specialists (v5). Roughly 150 lines of code for a system that plans, searches, analyzes, writes, and recovers from failures. You designed every node, every edge, every routing condition. You understand it because you built it.
Now you're going to reimplement that same system as a Deep Agent. ~40 lines. The same functional result — multi-source research with a consolidated report — but with a fundamental trade-off: you gained development speed and gave up control over each decision in the flow.
The value of this project isn't the code. It's the comparison. Putting v5 and v6 side by side and being able to say: "I gained X, I lost Y, and I'd choose Z for this use case." That's the judgment that turns you into an AI Engineer. Not the person who knows how to use a framework — the person who knows how to pick the right one.
Goal of the project
Reimplement the AI Research Assistant as a Deep Agent (v6) and compare it directly with the LangGraph version (v5), articulating the trade-offs of each approach.
By the end of this project:
- You'll create a Deep Agent with planning, filesystem, subagent spawning, and memory configured
- You'll run the same query on v5 (LangGraph) and v6 (Deep Agent) to compare the results
- You'll articulate exactly what you gained (development speed, simplicity) and what you lost (control, debuggability)
- You'll have TWO working versions of the Research Assistant in your portfolio
Before and after
v5 (Module 10): ~150 lines, total control
research-assistant-v5/
├── agents/
│ ├── researcher.py ← Agent specialized in search
│ ├── analyst.py ← Agent specialized in analysis
│ ├── writer.py ← Agent specialized in writing
│ └── supervisor.py ← The system's coordinator
├── tools/
│ ├── search_tools.py ← web_search, arxiv_search
│ ├── analysis_tools.py ← compare_sources, detect_patterns
│ └── writing_tools.py ← format_report
├── state/
│ └── multi_agent_state.py ← Typed shared state
├── tracing/
│ ├── agent_logger.py ← Per-agent logging
│ └── flow_tracer.py ← Flow tracing
└── main.py ← Orchestration + CLI
You designed: which agent does what, in what order, when to retry, when to ask for approval, how to log, and how to combine results.
v6 (this project): ~40 lines, the framework decides
research-assistant-v6/
├── .env ← API keys
├── requirements.txt ← deep-agents, langchain-openai, python-dotenv
├── agent_memory/ ← Memory directory (auto-generated)
└── main.py ← The whole agent in one file
The framework decides: how to break the task down, which files to create, when to delegate, and how to organize the results.
Technical specs
| Component | Version | Purpose |
|---|---|---|
| Python | 3.11+ | Runtime |
| deep-agents | v0.2+ | The Deep Agents framework |
| langchain-openai | latest | Model provider |
| langchain-community | latest | TavilySearchResults |
| python-dotenv | latest | Environment variables |
Installation
pip install deep-agents langchain-openai langchain-community python-dotenv
Environment variables
# .env
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
Step 1: Define the Deep Agent with planning
The first step is creating the agent with instructions that steer the planning. The instructions are the equivalent of designing the workflow in LangGraph — instead of nodes and edges, you write down what you want the agent to do.
"""
main.py
Research Assistant v6 — Deep Agent version.
"""
from dotenv import load_dotenv
load_dotenv()
from deep_agents import create_deep_agent
from deep_agents.memory import FilesystemMemoryBackend
from langchain_community.tools import TavilySearchResults
web_search = TavilySearchResults(max_results=5)
memory = FilesystemMemoryBackend(
base_path="./agent_memory",
max_memories=200,
max_memory_age_days=90,
)
agent = create_deep_agent(
"openai:gpt-4.1",
tools=[web_search],
name="Research Assistant v6",
instructions=(
"You are a specialized research assistant. "
"For every research topic: "
"1) Break the research into clear steps with write_todos. "
"2) Search diverse sources: general web, technical articles, reports. "
"3) Write the findings from each search into separate files inside research/. "
"4) If the topic has multiple dimensions, delegate searches to specialized subagents. "
"5) Analyze and synthesize every finding into analysis/synthesis.md. "
"6) Generate a structured final report at output/report.md with: "
" - Executive summary "
" - Key findings "
" - Analysis "
" - Conclusions "
" - Sources consulted "
"Remember the user's preferences across sessions."
),
memory=memory,
max_iterations=15,
)
What happens when the agent gets a task
When you run agent.run("Research the state of AI agents in production in 2025"), the agent:
- Reads relevant memories — if the user has researched before, it recovers their preferences
- Runs write_todos — it breaks the task down:
Automatically generated todos:
1. [pending] Define the scope: AI agents in production, 2025
2. [pending] Search for the main agent frameworks
3. [pending] Search for documented production use cases
4. [pending] Search for reported challenges and limitations
5. [pending] Synthesize the findings
6. [pending] Generate the final report
- Runs each step — searching, writing files, and updating todos
- Replans if it needs to — if a search reveals an unexpected dimension, it adds steps
Comparison with v5
In v5, you defined this workflow as a graph:
builder.add_edge(START, "plan")
builder.add_edge("plan", "researcher")
builder.add_edge("researcher", "analyst")
builder.add_edge("analyst", "writer")
builder.add_conditional_edges("writer", evaluate_quality, ...)
In v6, the equivalent is the instructions section. The agent translates your instructions into an execution plan. Less precise than explicit edges, but more flexible — if the task changes, the plan adapts without you touching any code.
Step 2: Configure the virtual filesystem
The virtual filesystem is the equivalent of graph state in LangGraph. In v5, the findings lived in state["findings"]. In v6, they live in files.
The expected file structure
When the agent finishes a research run, the filesystem looks like this:
workspace/
├── research/
│ ├── web_general.md ← Findings from the web search
│ ├── frameworks.md ← Information about specific frameworks
│ └── production_cases.md ← Production use cases
├── analysis/
│ └── synthesis.md ← Cross-source analysis
└── output/
└── report.md ← Consolidated final report
The advantage: context offloading
In v5, every finding lives in state["findings"] — a list that grows with each search. With 10 sources at 500 tokens each, that's 5,000 tokens sitting in the context window permanently.
In v6, each finding lives in a file. The agent reads only the file it needs for the current step. The context window stays lean.
v5 (LangGraph):
Context window: [system prompt] + [state with all the findings] + [current instruction]
Size: grows with every step
v6 (Deep Agent):
Context window: [system prompt] + [current file] + [current instruction]
Size: constant (~the same number of tokens per step)
Inspecting the generated files
result = agent.run("Research AI agents in production in 2025")
for path, content in result.files.items():
print(f"\n{'='*60}")
print(f"File: {path}")
print(f"Size: {len(content)} characters")
print(f"Preview: {content[:200]}...")
# Expected output (varies by model):
# ============================================================
# File: research/web_general.md
# Size: 2340 characters
# Preview: # Web Search: AI Agents in Production
#
# ## Sources found
# 1. **LangChain Blog** — "Agents in Production: Lessons Learned"
# Key insight: Most agents in production use...
#
# ============================================================
# File: research/frameworks.md
# Size: 1890 characters
# Preview: # AI Agent Frameworks
#
# ## Leading frameworks in 2025
# | Framework | Approach | Adoption |...
#
# ============================================================
# File: analysis/synthesis.md
# Size: 3100 characters
# Preview: # Synthesis: AI Agents in Production (2025)
#
# ## Patterns identified
# 1. Most production implementations are...
#
# ============================================================
# File: output/report.md
# Size: 4200 characters
# Preview: # AI Agents in Production: The Current State (2025)
#
# ## Executive Summary
# AI agents have moved from demos to production...
Step 3: Enable subagent spawning
In v5, the specialized agents (researcher, analyst, writer) were defined at build time. You decided which agent existed and what it did.
In v6, the main agent decides at runtime whether it needs to delegate. The instruction "delegate searches to specialized subagents when the topic is broad" gives it permission, not an obligation.
How it works under the hood
Main Agent: "Research AI agents in production"
│
├─ write_todos: 6 steps
│
├─ Step 1: Define scope → runs it directly
│
├─ Step 2: "Search for frameworks" → decides this one is specific
│ └─ spawn: framework_researcher
│ └─ Instructions: "Find the leading AI agent frameworks in 2025"
│ └─ Tools: [web_search]
│ └─ Returns: findings about LangGraph, CrewAI, AutoGen...
│ └─ Main Agent writes the result to research/frameworks.md
│
├─ Step 3: "Search for production cases" → decides it's different from step 2
│ └─ spawn: production_researcher
│ └─ Instructions: "Find companies using AI agents in production"
│ └─ Tools: [web_search]
│ └─ Returns: findings about real cases...
│ └─ Main Agent writes the result to research/production_cases.md
│
├─ Steps 4-6: Synthesize and generate the report → runs them directly
│
└─ Result: 6/6 todos completed, 2 subagents spawned
Control over subagents
agent = create_deep_agent(
"openai:gpt-4.1",
tools=[web_search],
name="Research Assistant v6",
instructions="...",
memory=memory,
max_iterations=15,
max_subagents=3,
subagent_timeout=60,
)
max_subagents=3— 3 concurrent subagents at most (prevents a cost explosion)subagent_timeout=60— each subagent gets 60 seconds before it's killed
Comparison with v5
| Aspect | v5 (LangGraph) | v6 (Deep Agent) |
|---|---|---|
| Agents defined at | Build time (in code) | Runtime (the framework decides) |
| Roles | Fixed: researcher, analyst, writer | Dynamic: the agent creates what it needs |
| Coordination | Supervisor with explicit edges | Main agent coordinates with write_todos |
| Control | You define who does what | The framework decides, you steer with instructions |
| Debugging | Per-agent logs, you know which node failed | Subagent spawning logs, less granular |
Step 4: Configure long-term memory
Memory is what lets the agent remember preferences across sessions. In v5, this needed a Store with namespaces and load/save logic. In v6, it's one line of configuration.
Configuration (already included in Step 1)
memory = FilesystemMemoryBackend(
base_path="./agent_memory",
max_memories=200,
max_memory_age_days=90,
)
Memory in action
Session 1:
result = agent.run(
"Research RAG techniques. I prefer academic sources from arxiv and NeurIPS."
)
print(result.output[:200])
# Expected output (varies by model):
# ## AI Research Report: RAG Techniques
#
# ### Executive Summary
# Retrieval-Augmented Generation techniques have evolved...
# (prioritizes arxiv and NeurIPS sources as requested)
Session 2 (days later):
result = agent.run("Research AI safety")
print(result.output[:200])
# Expected output (varies by model):
# ## AI Research Report: AI Safety
#
# ### Executive Summary
# The AI safety field has seen significant advances...
# (automatically prioritizes arxiv and NeurIPS thanks to the memory from session 1)
You never repeated the preference. The agent pulled it from the filesystem backend.
Inspecting the memories
import json
import os
memory_dir = "./agent_memory/memories"
if os.path.exists(memory_dir):
for f in os.listdir(memory_dir):
with open(os.path.join(memory_dir, f)) as fh:
mem = json.load(fh)
print(f" [{mem['type']}] {mem['content'][:80]}...")
# Expected output:
# [preference] The user prefers academic sources from arxiv and NeurIPS...
Step 5: The complete code (~40 lines)
This is the whole agent. Compare it mentally with the ~150 lines of v5.
"""
main.py
Research Assistant v6 — Deep Agent version.
A complete reimplementation of the Research Assistant as a Deep Agent.
"""
from dotenv import load_dotenv
load_dotenv()
from deep_agents import create_deep_agent
from deep_agents.memory import FilesystemMemoryBackend
from langchain_community.tools import TavilySearchResults
web_search = TavilySearchResults(max_results=5)
memory = FilesystemMemoryBackend(
base_path="./agent_memory",
max_memories=200,
max_memory_age_days=90,
)
agent = create_deep_agent(
"openai:gpt-4.1",
tools=[web_search],
name="Research Assistant v6",
instructions=(
"You are a specialized research assistant. "
"For every research topic: "
"1) Break the research into clear steps with write_todos. "
"2) Search diverse sources: general web, technical articles, reports. "
"3) Write the findings from each search into separate files inside research/. "
"4) If the topic has multiple dimensions, delegate searches to specialized subagents. "
"5) Analyze and synthesize every finding into analysis/synthesis.md. "
"6) Generate a structured final report at output/report.md with: "
" Executive summary, Key findings, Analysis, Conclusions, Sources. "
"Remember the user's preferences across sessions."
),
memory=memory,
max_iterations=15,
max_subagents=3,
subagent_timeout=60,
)
if __name__ == "__main__":
topic = input("Research topic: ")
result = agent.run(topic)
print(f"\n{'='*60}")
print(f"Research complete")
print(f"{'='*60}")
print(f"Files generated: {list(result.files.keys())}")
print(f"Todos: {sum(1 for t in result.todos if t['status'] == 'completed')}/{len(result.todos)}")
print(f"Subagents used: {len(result.subagents_spawned)}")
print(f"\nFinal report:")
print(result.files.get("output/report.md", "No report was generated")[:500])
python main.py
# Input: Research the state of AI agents in production in 2025
# Expected output:
# ============================================================
# Research complete
# ============================================================
# Files generated: ['research/web_general.md', 'research/frameworks.md', 'research/production_cases.md', 'analysis/synthesis.md', 'output/report.md']
# Todos: 6/6
# Subagents used: 2
#
# Final report:
# # AI Agents in Production: The Current State (2025)
#
# ## Executive Summary
# AI agents have moved from prototypes to production...
40 lines. Planning, filesystem, subagents, memory, and a CLI — all included.
Step 6: The side-by-side comparison
This is THE moment of the module. Put both versions face to face.
Lines of code
v5 (LangGraph):
state/multi_agent_state.py ← 25 lines
agents/researcher.py ← 35 lines
agents/analyst.py ← 30 lines
agents/writer.py ← 25 lines
agents/supervisor.py ← 40 lines
tools/*.py ← 45 lines
tracing/*.py ← 30 lines
main.py ← 50 lines
─────────────────────────────────────────
Total: ~280 lines (with tracing), ~150 without it
v6 (Deep Agent):
main.py ← 40 lines
─────────────────────────────────────────
Total: ~40 lines
The analysis table
| Aspect | v5 (LangGraph) | v6 (Deep Agent) | Winner |
|---|---|---|---|
| Lines of code | ~150 (without tracing) | ~40 | v6 (73% less code) |
| Development time | ~2-4 hours | ~20 minutes | v6 |
| Control over the flow | Total — every node and edge defined | Partial — instructions steer it | v5 |
| Debugging | Excellent — per-agent logs, per-node state | Acceptable — planning logs | v5 |
| Conditional branching | Yes — route_by_quality, evaluate | No — the framework decides | v5 |
| HITL | Granular — interrupt() at specific points | General — you approve the task | v5 |
| Multi-agent | Fixed roles, explicit coordination | Dynamic, the framework decides | Depends |
| Persistence | Checkpointer + Store | Filesystem + memory backend | Tie |
| Scalability | Manual — you add nodes/edges | Automatic — the agent adapts | v6 |
| Cost per run | Medium (~$0.05-0.10) | High (~$0.10-0.30) | v5 |
| Maintainability | More code = more to maintain | Less code = less to maintain | v6 |
| Testing | Unit tests per node | Integration tests on the result | v5 |
Trade-off analysis
What you gained with v6:
- ✅ Development speed: 20 minutes vs 2-4 hours. If you need a quick prototype, v6 wins
- ✅ Simplicity: 40 lines that a new developer understands in 5 minutes
- ✅ Adaptability: if the task changes, you only edit the instructions. In v5, you edit nodes, edges, and conditions
- ✅ Filesystem: the files are inspectable, you don't need to reach into the graph's state
- ✅ Built-in memory: one line vs ~80 lines of manual setup
What you lost with v6:
- ❌ Fine-grained control: you don't get to say "if quality_score < 0.7, search for more sources." The framework decides when to search again
- ❌ Granular HITL: you can't request approval before the analyst produces conclusions. You approve the whole task or nothing
- ❌ Precise debugging: in v5, you know that "the analyst received 8 correct findings but its conclusion was wrong." In v6, you see that "the agent generated a report with an error"
- ❌ Unit testing: in v5, you test researcher_node() on its own. In v6, you only test the final result
- ❌ Predictable costs: v5 has a fixed number of LLM calls (plan + search + analyze + write). v6 has a variable number (it depends on the agent's planning)
- ❌ Per-step models: in v5, the analyst uses gpt-4.1 and everyone else uses gpt-4.1-mini. In v6, it's the same model for everything
A comparative run
Run the same query on both versions:
from dotenv import load_dotenv
load_dotenv()
import time
QUERY = "Research the state of AI agents in production in 2025"
print("=" * 60)
print("v6 (Deep Agent)")
print("=" * 60)
start = time.time()
from deep_agents import create_deep_agent
from deep_agents.memory import FilesystemMemoryBackend
from langchain_community.tools import TavilySearchResults
agent_v6 = create_deep_agent(
"openai:gpt-4.1",
tools=[TavilySearchResults(max_results=5)],
name="Research Assistant v6",
instructions=(
"Research complex topics. Break them into steps with write_todos. "
"Write findings into files. Delegate to subagents when necessary. "
"Generate a final report at output/report.md."
),
memory=FilesystemMemoryBackend(base_path="./memory_v6"),
max_iterations=15,
)
result_v6 = agent_v6.run(QUERY)
time_v6 = time.time() - start
print(f"Time: {time_v6:.1f}s")
print(f"Files: {len(result_v6.files)}")
print(f"Todos: {sum(1 for t in result_v6.todos if t['status'] == 'completed')}/{len(result_v6.todos)}")
print(f"Subagents: {len(result_v6.subagents_spawned)}")
print(f"Report: {len(result_v6.files.get('output/report.md', ''))} chars")
# Expected output (varies):
# Time: ~45-90s
# Files: 4-5
# Todos: 5-6/5-6
# Subagents: 1-3
# Report: ~3000-5000 chars
The quantitative comparison gives you concrete data to articulate the trade-offs.
Success criteria
Your project is complete when you can answer YES to every question:
- ✅ Does the Deep Agent generate a working research report?
- ✅ Does the agent use write_todos to break the task down?
- ✅ Do the findings get written to separate files (rather than everything in the context window)?
- ✅ Does the agent spawn at least one subagent for specialized searches?
- ✅ Does the memory persist preferences across sessions?
- ✅ Can you articulate 3 advantages and 3 disadvantages of v6 vs v5?
- ✅ Do you have both versions (v5 and v6) working?
Test scenarios
Run these scenarios to validate that your agent works across different kinds of tasks.
Scenario 1: Simple research
result = agent.run("What is the current state of LangGraph?")
assert len(result.todos) >= 3, "It should plan at least 3 steps"
assert "output/report.md" in result.files, "It should generate a report"
print("✅ Scenario 1 passed")
Scenario 2: Multi-dimensional research
result = agent.run(
"Compare 3 RAG approaches: naive RAG, advanced RAG, and modular RAG. "
"For each one, research: architecture, pros, cons, and use cases."
)
assert len(result.files) >= 3, "It should generate multiple research files"
assert len(result.subagents_spawned) >= 1, "It should delegate at least one search"
print("✅ Scenario 2 passed")
Scenario 3: Memory across sessions
result1 = agent.run("Research AI safety. I prefer papers from arxiv.")
result2 = agent.run("Research AI governance")
report = result2.files.get("output/report.md", "")
print("✅ Scenario 3 passed" if "arxiv" in report.lower() else "❌ Memory isn't working")
Scenario 4: Re-planning
result = agent.run(
"Research the impact of AI on higher education. "
"If you find significant differences between regions, "
"research Europe and Latin America separately."
)
assert len(result.todos) > 5, "It should have replanned with more steps"
print("✅ Scenario 4 passed")
Common mistakes
1. The agent generates no files — it just answers directly
Cause: the instructions don't emphasize using the filesystem enough.
Fix: be explicit in the instructions:
instructions=(
"ALWAYS write the findings to files. "
"NEVER put the whole research into a single answer. "
"Each source → one file in research/. "
"The final report → output/report.md."
)
2. Too many subagents — costs blow up
Cause: you didn't cap max_subagents, or the instructions are too broad.
Fix:
agent = create_deep_agent(
...,
max_subagents=3,
subagent_timeout=60,
instructions=(
"Delegate to subagents ONLY when the topic has clearly "
"different dimensions that require specialized searches."
),
)
3. Memory doesn't persist between runs
Cause: you're creating a new FilesystemMemoryBackend with a different path every time.
Fix: always use the same base_path:
memory = FilesystemMemoryBackend(base_path="./agent_memory")
4. The plan has too many steps — it gets slow
Cause: the instructions are too detailed and the agent creates one step per sub-instruction.
Fix: simplify the instructions and cap the iterations:
agent = create_deep_agent(
...,
max_iterations=10,
instructions=(
"Research the topic in 4-5 steps max. "
"Don't over-decompose."
),
)
5. The report is shallow compared to v5
Cause: a single model is handling everything. In v5, gpt-4.1 did the deep analysis.
Fix: use gpt-4.1 (not mini) as the main model for complex research:
agent = create_deep_agent("openai:gpt-4.1", ...)
The trade-off: more expensive, but better reasoning quality.
6. You can't compare with v5 because you don't have it working
Cause: you didn't finish the Module 10 project.
Fix: the comparison is the whole point of this project. If you don't have v5, go back to Module 10 and finish it. The comparison without both versions loses all of its teaching value.
7. Filesystem files get mixed up between runs
Cause: you don't clean the workspace between different runs.
Fix: use a different --output-dir per research run, or clean it manually:
import shutil
if os.path.exists("./workspace"):
shutil.rmtree("./workspace")
8. The agent ignores the formatting instructions
Cause: the instructions are too long and the model loses the context.
Fix: put the most important instructions first in the prompt:
instructions=(
"FORMAT: A report with Summary, Findings, Analysis, Conclusions, Sources. "
"ALWAYS use write_todos. ALWAYS write to files. "
"Then: search diverse sources, delegate when necessary."
)
The portfolio angle
You now have TWO versions of the Research Assistant:
Your portfolio:
├── Research Assistant v5 (LangGraph)
│ ├── ~150 lines of code
│ ├── 4 specialized agents + a supervisor
│ ├── Custom workflow with edges and conditions
│ ├── Granular HITL, retry with quality evaluation
│ └── Shows: "I can build complex systems from scratch"
│
└── Research Assistant v6 (Deep Agent)
├── ~40 lines of code
├── Planning, filesystem, subagents, memory
├── The framework decides the flow
└── Shows: "I can use high-level tools efficiently"
In an interview, you can say:
"I built the same Research Assistant two ways. The LangGraph version is 150 lines — I designed every node, edge, and condition. The Deep Agent version is 40 lines — the framework handles planning, filesystem, and subagents. I can articulate the trade-offs: LangGraph gives me total control over debugging, HITL, and flow. Deep Agents gives me development speed and adaptability. I pick based on the use case."
That shows judgment, not dependence on a framework.
What's coming: Module 12 — LangSmith and Production
Your Research Assistant now exists in two versions. Both work. But neither one is production-ready.
How much does one research run cost? How many tokens does each step use? Is the report consistently good, or does it vary? How do you detect when the agent "hallucinates" a source? How do you measure whether v5 produces better reports than v6?
Module 12 adds LangSmith: complete tracing of every LLM call, automated evaluation of report quality, per-session token tracking, rate limiting, and a production checklist. It works with both versions — LangGraph and Deep Agent.
M11: You built the agent (v5 and v6)
↓
M12: You make it observable and deployable
↓
Result: an agent in production that you can monitor, evaluate, and improve
It's the module that turns your learning project into a system you could actually deploy.
Project resources
- Deep Agents — Documentation — Complete API reference for create_deep_agent
- Deep Agents — Planning — Documentation for write_todos and re-planning
- Deep Agents — Filesystem — The virtual filesystem API and best practices
- LangGraph vs Deep Agents — Official article on when to use each abstraction
- Building Effective Agents (Anthropic) — A decision framework for agent complexity
- Cognitive Architectures for Language Agents — Paper on planning, memory, and tool use in agents
Module 11 — LangChain & LangGraph: From Chains to Agents