{"slug": "understanding-middleware-in-deep-agents-with-runnable-examples", "title": "Understanding Middleware in Deep Agents (With Runnable Examples)", "summary": "Deep Agents, an open-source framework, provides a middleware stack that handles context windows, interrupted tool calls, task tracking, sub-agents, and file management automatically. The middleware runs in a specific order, with components like TodoListMiddleware and PatchToolCallsMiddleware, allowing developers to add custom logic without modifying the core agent loop.", "body_md": "If you've built even a simple AI agent, you've probably noticed that the \"agent loop\" itself is deceptively simple: the model gets a message, decides whether to call a tool, gets the result back, and repeats until it has an answer. But real-world agents need a lot more than that bare loop to actually work well.\n\nWhat happens when a conversation gets so long it blows past the model's context window? What if a tool call gets interrupted halfway through and leaves your message history in a broken state? What if you want the agent to keep a running todo list of what it's working on, or delegate parts of a task to a specialized sub-agent, or read and write files as part of its job?\n\nYou could bolt all of this onto your agent manually. Or, if you're using Deep Agents, you get most of it for free through something called **middleware**.\n\nThis post walks through what middleware actually is, why Deep Agents ships with a default stack of it, and how each piece behaves, with runnable code for each one so you can see it working instead of just reading about it.\n\nIf you've done any web development, the term \"middleware\" probably already rings a bell. It's the same idea here.\n\nMiddleware is code that sits *around* the core agent loop and gets a chance to run before or after certain things happen, like before a tool call executes, after the model responds, or right before messages are sent to the model. Instead of writing all of this logic directly inside your agent, you attach separate, independent pieces of middleware that each handle one specific concern.\n\nThis matters for two reasons:\n\nThink of it like a pipeline. A request comes in, passes through a stack of middleware (each one doing its own small job), reaches the model, and the response passes back out through that same stack on the way out.\n\nWhen you call `create_deep_agent`\n\n, you're not getting a bare-bones agent loop. You're getting an agent that already knows how to track tasks, manage files, spin up sub-agents, keep conversations within context limits, and recover gracefully from interrupted tool calls, all because of a default middleware stack that's assembled for you automatically.\n\nHere's the order that stack runs in, from first to last:\n\n`skills`\n\n) injects skill metadata before file tools runAfter that, there's room for your own custom middleware, followed by a \"tail\" of provider-specific extras: things like prompt caching for Anthropic or Bedrock models, memory injection, excluded-tool filtering, and human-in-the-loop approval steps.\n\nThe important thing to understand up front is that **order matters**. Each middleware runs at a specific point for a reason, for example, `PatchToolCallsMiddleware`\n\nneeds to run before prompt caching so the cached message prefix actually matches what gets sent to the model. We'll come back to details like this as we go through each piece.\n\nRather than explain all of this in the abstract, the rest of this post goes through each middleware one at a time with a runnable example, so you can actually see:\n\nBy the end, you should have a clear mental model of what's happening under the hood every time you call `create_deep_agent`\n\n, and enough understanding to start adding your own custom middleware or overriding the defaults when you need something different.\n\nLet's start with the first one in the stack: `TodoListMiddleware`\n\n.\n\n``` python\n\"\"\"\nExample: TodoListMiddleware\n\"\"\"\n\nimport os\nimport sys\n\nsys.stdout.reconfigure(encoding=\"utf-8\")\nsys.stderr.reconfigure(encoding=\"utf-8\")\n\nfrom dotenv import load_dotenv\nload_dotenv()\n\nfrom langchain.tools import tool\nfrom langchain_nvidia_ai_endpoints import ChatNVIDIA\nfrom deepagents import create_deep_agent\n\n@tool\ndef get_weather(city: str) -> str:\n    \"\"\"Get the current weather in a city.\"\"\"\n    return f\"The weather in {city} is sunny, 24C.\"\n\n@tool\ndef get_time(city: str) -> str:\n    \"\"\"Get the current time in a city.\"\"\"\n    return f\"The current time in {city} is 12:00 PM.\"\n\n@tool\ndef get_population(city: str) -> str:\n    \"\"\"Get the approximate population of a city.\"\"\"\n    return f\"The population of {city} is approximately 14 million.\"\n\n# Build the model directly so we can control the timeout\nmodel = ChatNVIDIA(\n    model=\"nvidia/nemotron-3-super-120b-a12b\",\n    api_key=os.environ[\"NVIDIA_API_KEY\"],\n)\n\nagent = create_deep_agent(\n    model=model,\n    tools=[get_weather, get_time, get_population],\n    system_prompt=\"You are a helpful assistant. For multi-step requests, \"\n                  \"plan your steps using the todo list before executing them.\",\n)\n\nresult = agent.invoke(\n    {\n        \"messages\": [\n            (\n                \"user\",\n                \"I need a full briefing on Tokyo: check the weather, the \"\n                \"current time, and the population. Plan this out first, \"\n                \"then give me a summary.\",\n            )\n        ]\n    },\n    config={\"configurable\": {\"thread_id\": \"todo-example-1\"}},\n)\n\nfor m in result[\"messages\"]:\n    if m.type == \"ai\":\n        if m.tool_calls:\n            for call in m.tool_calls:\n                print(f\"[Tool Call] {call['name']} -> {call['args']}\")\n        if m.content:\n            print(f\"AI: {m.content}\")\n    elif m.type == \"tool\":\n        print(f\"[Tool Result - {m.name}]: {m.content}\")\n\nif \"todos\" in result:\n    print(\"\\nFinal todo list state:\")\n    for todo in result[\"todos\"]:\n        print(f\"  - {todo}\")\n\"\"\"\nExample: SkillsMiddleware\n\nThe SkillsMiddleware is added to the default stack automatically when you pass\n`skills=` to `create_deep_agent`. It loads each skill's `name` and `description`\nfrom the YAML frontmatter of `SKILL.md` into the system prompt at startup, and\ngives the agent a `read_file` tool so it can pull the full instructions in\nprogressive disclosure when a task matches a skill.\n\nThis example uses `FilesystemBackend` so skills are loaded from disk relative\nto a project root. Two sample skills live under `middleware-examples/skills/`:\n\n  - code-review: reviews code for bugs, security, performance, and style.\n  - git-commit:  writes conventional commit messages from a diff or summary.\n\nRun from the project root with:\n    uv run middleware-examples/02_skills_middleware.py\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\nsys.stdout.reconfigure(encoding=\"utf-8\")\nsys.stderr.reconfigure(encoding=\"utf-8\")\n\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nfrom langchain_nvidia_ai_endpoints import ChatNVIDIA\n\nfrom deepagents import create_deep_agent\nfrom deepagents.backends.filesystem import FilesystemBackend\n\nROOT = Path(__file__).resolve().parent.parent\nSKILLS_DIR = ROOT / \"middleware-examples\" / \"skills\"\n\nbackend = FilesystemBackend(root_dir=str(ROOT))\n\nllm = ChatNVIDIA(\n    model=\"nvidia/nemotron-3-super-120b-a12b\",\n    timeout=120,\n)\n\nagent = create_deep_agent(\n    model=llm,\n    backend=backend,\n    skills=[str(SKILLS_DIR)],\n    system_prompt=(\n        \"You are a helpful assistant. When a user request matches one of your \"\n        \"available skills, read the skill's SKILL.md and follow its instructions.\"\n    ),\n)\n\nresult = agent.invoke(\n    {\n        \"messages\": [\n            (\n                \"user\",\n                \"Please turn this diff into a commit message:\\n\"\n                \"+ added 5 req/sec rate limiting to /login\\n\"\n                \"+ added unit tests for the limiter\\n\"\n                \"- removed the in-memory counter fallback\",\n            ),\n            (\n                \"user\",\n                \"review the below code:\\n\"\n                \"def add(a, b):\\n\"\n                \"    return a.tO_string() + b\",\n            ),\n        ]\n    },\n    config={\"configurable\": {\"thread_id\": \"skills-example-1\"}},\n)\n\nprint(\"\\n=== Skill state ===\")\nfor m in result[\"messages\"]:\n    if m.type == \"ai\" and m.content:\n        print(f\"\\nAI: {m.content}\")\n\"\"\"\nExample: FilesystemMiddleware\n\nThe FilesystemMiddleware handles file system operations such as reading,\nwriting, and navigating directories. When you pass permissions, filesystem\npermissions enforcement is included.\n\nThis example shows basic filesystem operations without custom permissions.\n\nRun with: python middleware-examples/03_filesystem_middleware.py\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\nsys.stdout.reconfigure(encoding=\"utf-8\")\nsys.stderr.reconfigure(encoding=\"utf-8\")\n\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nfrom langchain_nvidia_ai_endpoints import ChatNVIDIA\n\nfrom deepagents import create_deep_agent\nfrom deepagents.backends.filesystem import FilesystemBackend\n\n# FIX: this was `.parent.parent`, which resolves to one directory *above*\n# your project (this file lives at <ROOT>/middleware-examples/<this file>,\n# so a single .parent already gets you back to <ROOT>). The extra .parent\n# silently wrote poem.txt outside the folder you were checking.\nROOT = Path(__file__).resolve().parent\n\n# FIX: virtual_mode=True still resolves relative paths under root_dir (same\n# as before) but also blocks path traversal ('..', '~') and absolute paths\n# escaping root_dir. virtual_mode=False gives the agent no real guardrails.\nbackend = FilesystemBackend(root_dir=str(ROOT), virtual_mode=True)\n\nllm = ChatNVIDIA(\n    model=\"nvidia/nemotron-3-super-120b-a12b\",\n    timeout=120,\n)\n\nagent = create_deep_agent(\n    model=llm,\n    backend=backend,\n    system_prompt=(\n        \"You are a helpful assistant. Use the filesystem to save and \"\n        \"read files as needed.\"\n    ),\n)\n\nresult = agent.invoke(\n    {\n        \"messages\": [\n            (\n                \"user\",\n                \"Write a short poem to a file called 'poem.txt'.\",\n            )\n        ]\n    },\n    config={\"configurable\": {\"thread_id\": \"fs-example-1\"}},\n)\n\n# DEBUG: log every message, including tool calls and tool results, not just\n# AI text. This is what would have made the original bug obvious immediately\n# — you'd have seen a write_file call and a WriteResult path, just not in\n# the folder you expected.\nfor m in result[\"messages\"]:\n    if m.type == \"ai\":\n        if getattr(m, \"tool_calls\", None):\n            for tc in m.tool_calls:\n                print(f\"TOOL CALL: {tc['name']}({tc['args']})\")\n        if m.content:\n            print(f\"AI: {m.content}\")\n    elif m.type == \"tool\":\n        print(f\"TOOL RESULT [{m.name}]: {m.content}\")\n\n# Sanity check: prove where the file actually landed.\nexpected_path = ROOT / \"poem.txt\"\nif expected_path.exists():\n    print(f\"\\n✅ poem.txt created at: {expected_path}\")\nelse:\n    print(f\"\\n⚠️  poem.txt not found at expected path: {expected_path}\")\n\"\"\"\nExample: SubAgentMiddleware\n\nThe SubAgentMiddleware spawns and coordinates subagents for delegating\ntasks to specialized agents. Only the parent agent exposes the task tool\nthat creates subagents.\n\nThis example creates a subagent for research. The parent agent delegates\nresearch to the subagent via the task tool.\n\nRun with: python middleware-examples/04_subagent_middleware.py\n\"\"\"\n\nimport os\nimport sys\n\nsys.stdout.reconfigure(encoding=\"utf-8\")\nsys.stderr.reconfigure(encoding=\"utf-8\")\n\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nfrom langchain_nvidia_ai_endpoints import ChatNVIDIA\n\nfrom deepagents import create_deep_agent, SubAgent\n\nllm = ChatNVIDIA(\n    model=\"nvidia/nemotron-3-super-120b-a12b\",\n    timeout=120,\n    model_kwargs={\"chat_template_kwargs\": {\"enable_thinking\": False}},\n)\n\nresearch_subagent = SubAgent(\n    name=\"research\",\n    description=\"Researches a topic and returns findings.\",\n    model=llm,\n    system_prompt=\"You are a research assistant. Find answers concisely.\",\n)\n\nwriter_subagent = SubAgent(\n    name=\"writer\",\n    description=\"Writes content, reports, or summaries based on research findings.\",\n    model=llm,\n    system_prompt=\"You are a skilled writer. Produce clear, well-structured content.\",\n)\n\nreviewer_subagent = SubAgent(\n    name=\"reviewer\",\n    description=\"Reviews content for quality, accuracy, and completeness.\",\n    model=llm,\n    system_prompt=\"You are a meticulous reviewer. Check for errors and suggest improvements.\",\n)\n\nagent = create_deep_agent(\n    model=llm,\n    subagents=[research_subagent, writer_subagent, reviewer_subagent],\n    system_prompt=(\n        \"You are a helpful assistant. Delegate tasks to subagents using the \"\n        \"task tool. Use 'research' for finding information, 'writer' for \"\n        \"creating content, and 'reviewer' for quality checks.\"\n    ),\n)\n\nresult = agent.invoke(\n    {\n        \"messages\": [\n            (\n                \"user\",\n                \"Research the history of Python, write a short summary, then review it.\",\n            )\n        ]\n    },\n    config={\"configurable\": {\"thread_id\": \"subagent-example-1\"}},\n)\n\nfor m in result[\"messages\"]:\n    if m.type == \"ai\" and m.content:\n        print(f\"AI: {m.content}\")\n```\n\n", "url": "https://wpnews.pro/news/understanding-middleware-in-deep-agents-with-runnable-examples", "canonical_source": "https://dev.to/syeedmdtalha/understanding-middleware-in-deep-agents-with-runnable-examples-36j7", "published_at": "2026-07-22 12:49:29+00:00", "updated_at": "2026-07-22 13:00:34.012485+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "artificial-intelligence"], "entities": ["Deep Agents", "Anthropic", "Bedrock"], "alternates": {"html": "https://wpnews.pro/news/understanding-middleware-in-deep-agents-with-runnable-examples", "markdown": "https://wpnews.pro/news/understanding-middleware-in-deep-agents-with-runnable-examples.md", "text": "https://wpnews.pro/news/understanding-middleware-in-deep-agents-with-runnable-examples.txt", "jsonld": "https://wpnews.pro/news/understanding-middleware-in-deep-agents-with-runnable-examples.jsonld"}}