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

ComponentVersionPurpose
Python3.11+Runtime
deep-agentsv0.2+The Deep Agents framework
langchain-openailatestModel provider
langchain-communitylatestTavilySearchResults
python-dotenvlatestEnvironment 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:

  1. Reads relevant memories — if the user has researched before, it recovers their preferences
  2. 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
  1. Runs each step — searching, writing files, and updating todos
  2. 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

Aspectv5 (LangGraph)v6 (Deep Agent)
Agents defined atBuild time (in code)Runtime (the framework decides)
RolesFixed: researcher, analyst, writerDynamic: the agent creates what it needs
CoordinationSupervisor with explicit edgesMain agent coordinates with write_todos
ControlYou define who does whatThe framework decides, you steer with instructions
DebuggingPer-agent logs, you know which node failedSubagent 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

Aspectv5 (LangGraph)v6 (Deep Agent)Winner
Lines of code~150 (without tracing)~40v6 (73% less code)
Development time~2-4 hours~20 minutesv6
Control over the flowTotal — every node and edge definedPartial — instructions steer itv5
DebuggingExcellent — per-agent logs, per-node stateAcceptable — planning logsv5
Conditional branchingYes — route_by_quality, evaluateNo — the framework decidesv5
HITLGranular — interrupt() at specific pointsGeneral — you approve the taskv5
Multi-agentFixed roles, explicit coordinationDynamic, the framework decidesDepends
PersistenceCheckpointer + StoreFilesystem + memory backendTie
ScalabilityManual — you add nodes/edgesAutomatic — the agent adaptsv6
Cost per runMedium (~$0.05-0.10)High (~$0.10-0.30)v5
MaintainabilityMore code = more to maintainLess code = less to maintainv6
TestingUnit tests per nodeIntegration tests on the resultv5

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

  1. Deep Agents — Documentation — Complete API reference for create_deep_agent
  2. Deep Agents — Planning — Documentation for write_todos and re-planning
  3. Deep Agents — Filesystem — The virtual filesystem API and best practices
  4. LangGraph vs Deep Agents — Official article on when to use each abstraction
  5. Building Effective Agents (Anthropic) — A decision framework for agent complexity
  6. Cognitive Architectures for Language Agents — Paper on planning, memory, and tool use in agents

Module 11 — LangChain & LangGraph: From Chains to Agents