I tested yet another implementation of a multi-agent orchestrator.
The paradigm for AI agents is shifting rapidly from single, monolithic prompt loops to structured multi-agent systems capable of division of labor, state persistence, and fine-grained tool orchestration.
Although IBM watsonx Orchestrate is my go-to solution for enterprise-level agent deployment, I wanted to experiment with LangChain's Deep Agents to evaluate its multi-agent orchestration capabilities.
Introduction: The Deep Agents Architecture
LangChain announced recently Managed Deep Agents, introducing a managed platform to execute deep agent topologies. The Deep Agents paradigm addresses the fundamental limitations of single-agent workflows—specifically context window degradation, tool confusion, and compounding reasoning failures during extended, multi-step tasks.
What is the Deep Agents Pattern?
The Deep Agents pattern is a hierarchical multi-agent architecture where:
- A top-level Orchestrator decomposes complex tasks and delegates sub-tasks to specialists.
- Each Specialist Agent runs its own isolated ReAct loop with its own tools and memory.
- Specialists return structured results to the orchestrator, which synthesises them.
- Agents can be composed recursively — an orchestrator can itself be a sub-agent.
This pattern adds genuine value over a single agent because:
| Single Agent | Deep Agents |
| ---------------------------------- | ------------------------------------------------- |
| One context window gets overloaded | Each agent has an isolated, focused context |
| All reasoning in one prompt | Specialised reasoning per domain |
| Hard to scale | Add specialists without changing the orchestrator |
| Hard to debug | Full delegation trace for auditability |
---
Excerpt from LangChain Deep Agents Documention;
Deep Agents overview
Deep Agents is the easiest way to start building agents and applications that are powered by LLMs—with built-in capabilities for file systems for context management, subagent-spawning, and long-term memory. Optional capabilities such as task planning and skills extend the harness when your use case needs them. You can use deep agents for any task, including complex, multi-step tasks.Deep Agents comes with the following capabilities:
- Take actions in an environment: Take actions via tools, read and write files, execute code
- Connect to your data: Load memories, skills, and domain knowledge at the right moment
- Manage growing context: Summarize history and offload large results across long runs
- Parallelize tasks: Delegate to general or specialized subagents running in isolated context windows
- Stay in the loop: Pause for human approval at critical decision points
- Improve over time: Update memory, skills, and prompts based on real usage
And from the Github repository;
Deep Agents is an open source agent harness — an opinionated agent that runs out of the box. Extend, override, or replace any piece.
Principles:
- Opinionated — defaults tuned for long-horizon, multi-step work
- Extensible — override or replace any piece without forking
- Model-agnostic — works with any LLM that supports tool calling: frontier, open-weight, or local
- Production-ready — built on LangGraph (streaming, persistence, checkpointing) with first-class tracing, evaluation, and deployment via LangSmith
Features include:
Sub-agents — delegate tasks to agents with isolated context windows
Filesystem — read, write, edit, or search over pluggable local, sandboxed, or remote backends
Context management — summarize long threads and offload tool outputs to disk
Shell access — run commands in your sandbox of choice
Persistent memory — pluggable state and store backends for cross-session recall
Human-in-the-loop — approve, edit, or reject tool calls before they run
Skills — reusable behaviors the agent can load on demand
Tools — bring your own functions or any MCP server
Core Concepts of LangChain Deep Agents
Hierarchical Orchestration: A primary agent acts as a supervisor, receiving high-level goals and delegating sub-tasks to dedicated specialist agents rather than executing every step sequentially in a single context loop.
Specialized Agent Roles: Individual agents operate with tightly scoped system prompts, specialized tools, and isolated context windows to execute sub-tasks (e.g., information retrieval, numeric calculations, or document composition).
Isolated Memory Stores: Each agent maintains its own short-term working memory, facts, and episodic memory, preventing state contamination across different phases of execution.
ReAct Reasoning Loop: Sub-agents execute autonomous Thought → Action → Observation loops using validated tool interfaces.
Technical Implementation
Now that the feature set of "Deep Agents" is done, let's jump into a simple didatical (for myself) implementation.
The implementation in the provided repository (scaffolded via IBM Bob) provides a complete local and cloud-compatible implementation of the Deep Agents pattern.
Architecture Overview
User Task
└─ OrchestratorAgent (Planning & Synthesis)
├─ ResearcherAgent (web_search)
├─ AnalystAgent (calculator, code_executor)
└─ WriterAgent (file_reader)
The system is structured across four primary execution layers:
Orchestration Layer (
agents/orchestrator.py): Accepts user input, evaluates task complexity, builds a plan, delegates sub-tasks sequentially, and synthesizes outputs.Specialist Agents Layer (
agents/specialists.py): ImplementsResearcherAgent,AnalystAgent, andWriterAgent.Tool Infrastructure (
tools/): Provides AST-safe arithmetic evaluation, isolated Python execution, web search abstraction, and sandboxed file I/O.Relay & UI Layer (
app.py&main.py): Offers a Streamlit interface with asynchronous cross-thread state syncing, live phase tracking, and a CLI execution harness.
Technical Stack
- The main application is written in Python using Streamlit for UI
- The back-end llm inference for local use is tested with Ollama but llama.cpp support and configraion is implemented as well.
Code Structure
Based on the requirements I expressed, hereafter is the current structure of the application;
deepagents-usecase/
├── agents/
│ ├── __init__.py # Package exports
│ ├── base.py # Abstract BaseAgent + ReAct loop
│ ├── llm_client.py # LLM adapter (Ollama/llama.cpp/OpenAI/Anthropic)
│ ├── messages.py # Typed message protocol
│ ├── orchestrator.py # OrchestratorAgent (top-level)
│ ├── researcher.py # ResearcherAgent specialist
│ ├── analyst.py # AnalystAgent specialist
│ └── writer.py # WriterAgent specialist
├── tools/
│ ├── __init__.py
│ ├── base.py # BaseTool + ToolResult
│ ├── calculator.py # Safe AST-based arithmetic evaluator
│ ├── code_executor.py # Sandboxed Python execution (exec with allow-list)
│ ├── file_reader.py # Sandboxed file reading (input/ only)
│ └── web_search.py # Web search (stub + live Tavily)
├── memory/
│ ├── __init__.py
│ └── store.py # AgentMemoryStore (short/long-term/episodic)
├── config/
│ ├── __init__.py
│ └── settings.py # Centralised env-based configuration
├── tests/
│ ├── test_tools.py # Tool unit tests (71 tests)
│ ├── test_memory.py # Memory unit tests (24 tests)
│ ├── test_agents.py # Agent integration tests (92 tests)
│ └── test_code_executor.py # Code Executor tests (134 tests)
├── input/ # Input documents (content gitignored)
├── output/ # Generated reports (content gitignored)
├── memory/ # Persistent agent memory (JSON files)
├── scripts/
│ ├── start.sh # Launch Streamlit in detached mode
│ ├── stop.sh # Gracefully stop the application
│ ├── cleanup.sh # Remove .venv, __pycache__, etc.
│ └── check_code_executor.py # Standalone Code Executor sanity-check
├── Docs/
│ ├── Architecture.md # Mermaid architecture diagrams
│ ├── Quickstart.md # Step-by-step getting started guide
│ ├── API.md # Full public API reference
│ └── CodeExecutorVerification.md # Code Executor test & verification guide
├── app.py # Streamlit web UI
├── main.py # CLI entry point
├── requirements.txt
├── .env.example # Environment variable template
└── README.md
Flexible configurations allow the codebase to easily adapt to standard use cases. The backend supports multiple LLM inference sources, including local runtimes like llama.cpp and Ollama, as well as cloud-hosted providers accessed via API keys.
# =============================================================================
# Deep Agents System — Environment Configuration
# =============================================================================
# Copy this file to .env and fill in your values.
# NEVER commit the .env file to version control.
#
# Usage:
# cp .env.example .env
# # edit .env with your actual keys
# =============================================================================
# ─── LLM Provider ──────────────────────────────────────────────────────────
# Select ONE provider. Comment out the others.
# Option A: Ollama (local, default — no API key needed)
LLM_PROVIDER=ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3.2
# Option B: llama.cpp (local)
# LLM_PROVIDER=llamacpp
# LLAMACPP_BASE_URL=http://localhost:9931/v1
# LLAMACPP_MODEL=local-model
# Option C: OpenAI
# LLM_PROVIDER=openai
# OPENAI_API_KEY=sk-...
# OPENAI_MODEL=gpt-4o
# Option D: Anthropic
# LLM_PROVIDER=anthropic
# ANTHROPIC_API_KEY=sk-ant-...
# ANTHROPIC_MODEL=claude-3-5-sonnet-20241022
# ─── Application Settings ──────────────────────────────────────────────────
APP_PORT=8501
APP_HOST=0.0.0.0
LOG_LEVEL=INFO
# ─── Agent Configuration ───────────────────────────────────────────────────
# Maximum reasoning steps per agent (lower = faster on slow local LLMs)
MAX_AGENT_STEPS=8
# Maximum tokens per LLM call (1024 is enough for ReAct; raise for longer reports)
MAX_TOKENS=1024
# Temperature for LLM responses (0.0 = deterministic, 1.0 = creative)
TEMPERATURE=0.1
# ─── Memory & Storage ──────────────────────────────────────────────────────
# Directory for persistent agent memory (relative to project root)
MEMORY_DIR=./memory
# Maximum number of memories to retain per agent
MAX_MEMORY_ENTRIES=100
# ─── Tool Configuration ────────────────────────────────────────────────────
# Enable or disable specific tools (true/false)
TOOL_WEB_SEARCH_ENABLED=true
TOOL_CALCULATOR_ENABLED=true
TOOL_FILE_READER_ENABLED=true
TOOL_CODE_EXECUTOR_ENABLED=true
# Web search stub — set to real Tavily/SerpAPI key for live search
# TAVILY_API_KEY=tvly-...
# ─── Output ────────────────────────────────────────────────────────────────
OUTPUT_DIR=./output
Key Code Excerpts
The application serves to illustrate multi-agent orchestration, highlighting how tasks are dynamically distributed and delegated across dedicated toolsets.
Some core implementation details are outlined below. Designed with extensibility in mind, the architecture easily accommodates custom tools and additional specialist agents.
Safe AST Arithmetic Evaluator (tools/calculator.py)
To prevent arbitrary code execution vulnerabilities in arithmetic tasks, the system evaluates mathematical expressions using Python's Abstract Syntax Tree (ast) module against an explicit whitelist of AST nodes and functions—avoiding raw eval() calls:
# Whitelisted AST operators and math functions
_SAFE_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.USub: operator.neg,
}
_SAFE_FUNCTIONS = {
"abs": abs, "round": round, "sqrt": math.sqrt,
"sin": math.sin, "cos": math.cos, "log": math.log,
"pi": math.pi, "e": math.e,
}
def _safe_eval(node: ast.expr) -> float:
"""Recursively evaluate an AST expression node in a safe sandbox."""
if isinstance(node, ast.Constant):
if isinstance(node.value, (int, float)):
return float(node.value)
raise ValueError(f"Unsupported constant type: {type(node.value).__name__}")
if isinstance(node, ast.BinOp):
op_type = type(node.op)
if op_type not in _SAFE_OPERATORS:
raise ValueError(f"Unsupported binary operator: {op_type.__name__}")
left = _safe_eval(node.left)
right = _safe_eval(node.right)
return _SAFE_OPERATORS[op_type](left, right)
if isinstance(node, ast.Call):
func_name = node.func.id
if func_name not in _SAFE_FUNCTIONS:
raise ValueError(f"Function '{func_name}' is not whitelisted.")
args = [_safe_eval(a) for a in node.args]
return _SAFE_FUNCTIONS[func_name](*args)
raise ValueError(f"Unsupported AST node type: {type(node).__name__}")
Sandboxed Python Code Executor (tools/code_executor.py)
For advanced data processing, the code executor provides an isolated environment with restricted built-ins, pre-imported scientific modules, output redirection, and thread timeout controls:
def _build_exec_namespace() -> dict[str, Any]:
return {
"__builtins__": _SAFE_BUILTINS, # Explicit whitelist (no open, __import__, eval)
"math": math,
"statistics": statistics,
"pi": math.pi,
"e": math.e,
}
def _execute_code(code: str, timeout: int = 5) -> ToolResult:
exec_result = _ExecResult()
captured_io = io.StringIO()
def _worker():
namespace = _build_exec_namespace()
namespace["__builtins__"]["print"] = lambda *args, **kw: print(*args, **{**kw, "file": captured_io})
try:
exec(code, namespace)
exec_result.stdout = captured_io.getvalue()
except Exception as exc:
exec_result.error = f"{type(exc).__name__}: {exc}"
thread = threading.Thread(target=_worker, daemon=True)
thread.start()
thread.join(timeout=timeout)
if thread.is_alive():
return ToolResult(success=False, error=f"Execution timed out after {timeout}s.")
Thread-Safe Streamlit UI Relay (app.py)
Streamlit worker threads cannot directly write to st.session_state due to missing execution contexts (ScriptRunContext). The application uses an @st.cache_resource singleton relay with reentrant locking (threading.RLock) to safely stream agent execution steps across thread boundaries into the web UI:
class _PipelineRelay:
"""Thread-safe relay store for the running multi-agent pipeline."""
def __init__(self) -> None:
self.lock = threading.RLock()
self.status_messages: list[dict[str, str]] = []
self.agent_cards: dict[str, list[str]] = {}
self.running: bool = False
def append_status(self, msg_dict: dict[str, str]) -> None:
with self.lock:
self.status_messages.append(msg_dict)
agent_id = msg_dict.get("agent_id", "orchestrator")
self.agent_cards.setdefault(agent_id, []).append(msg_dict.get("detail", ""))
@st.cache_resource
def _get_relay() -> _PipelineRelay:
return _PipelineRelay()
Example Use Case: Climate Change Analysis
Input task:
"Analyse the impact of rising CO2 concentrations on global ecosystems. Include key statistics such as current CO2 levels and the percentage increase since pre-industrial times. Conclude with policy recommendations."
What happens:
- Orchestrator decomposes the task into research + analysis + writing.
- Researcher runs 3 web searches: CO2 trends, ecosystem impact, policy frameworks.
-
Analyst uses the calculator tool:
(421 - 280) / 280 * 100 = 50.36%increase. - Writer produces a structured Markdown document with executive summary, findings, data analysis, and recommendations.
- Orchestrator synthesises all outputs into the final report.
Output is saved to output/deep_agents_report_<timestamp>.md.
Benefits of Renewable Energy and Calculation of 280 Times 42
Benefits of Renewable Energy
Renewable energy reduces greenhouse gas emissions, contributing to climate change mitigation (Source: [National Renewable Energy Laboratory](https://www.nrel.gov/renewables/energy-benefits.html)). This is a key finding supported by credible sources, including the International Renewable Energy Agency (2020) and the World Health Organization (2020).
Renewable energy creates jobs and stimulates local economies (Source: [International Renewable Energy Agency](https://www.irena.org/publications/2020/Jun/Global-Status-Report-2020)). This is a significant benefit highlighted by the International Energy Agency (2020) and the National Bureau of Economic Research (2020).
Renewable energy improves air quality and public health (Source: [World Health Organization](https://www.who.int/news-room/fact-sheets/detail/air-pollution)). This is a critical aspect of renewable energy, supported by the National Renewable Energy Laboratory (n.d.).
The global renewable energy market is projected to reach 30% of total energy production by 2025 (Source: [International Energy Agency](https://www.iea.org/news/pressrelease/2020/june/global-renewables-report-2020/)). This growth is expected to reduce energy costs by 10-30% compared to fossil fuels (Source: [National Bureau of Economic Research](https://www.nber.org/papers/w28822)).
Calculation of 280 Times 42
The calculation of 280 times 42 yields 11,840. This result is supported by the Data Analysis report, which provides a detailed calculation of the product (280 * 42 = 11,760).
Trend Analysis
The global renewable energy market is expected to grow significantly, with a projected 30% share of total energy production by 2025. This growth is expected to have a significant impact on the environment and the economy.
Key Insights
1. Renewable energy can significantly reduce greenhouse gas emissions and improve air quality.
2. Renewable energy can create jobs and stimulate local economies.
Limitations
The data provided is based on projections and may not reflect actual outcomes.
Sources
1. National Renewable Energy Laboratory. (n.d.). Energy Benefits of Renewable Energy. Retrieved from <https://www.nrel.gov/renewables/energy-benefits.html>
2. International Renewable Energy Agency. (2020). Global Status Report 2020. Retrieved from <https://www.irena.org/publications/2020/Jun/Global-Status-Report-2020>
3. World Health Organization. (2020). Air pollution. Retrieved from <https://www.who.int/news-room/fact-sheets/detail/air-pollution>
4. International Energy Agency. (2020). Global Renewables Report 2020. Retrieved from <https://www.iea.org/news/pressrelease/2020/june/global-renewables-report-2020/>
5. National Bureau of Economic Research. (2020). The Economics of Renewable Energy. Retrieved from <https://www.nber.org/papers/w28822>
Adding a New Specialist Agent
As noted earlier, the application is designed to easily accommodate new tools. The section below provides a concrete example demonstrating this extensibility.
- Create
agents/my_specialist.pysubclassingBaseAgent. - Implement the
role_descriptionproperty. - Register the agent in
OrchestratorAgent.__init__. - Add delegation logic in
OrchestratorAgent.run_task.
Example skeleton:
from agents.base import BaseAgent
class MySpecialistAgent(BaseAgent):
def __init__(self, llm_client=None, on_status=None):
super().__init__(
agent_id="my_specialist",
tools=[my_custom_tool],
llm_client=llm_client,
on_status=on_status,
)
@property
def role_description(self) -> str:
return "You are a specialist that does X…"
Conclusion
This codebase demonstrates how the Deep Agents design pattern can be built in practice. By combining hierarchical agent delegation with AST-sandboxed tool safety and thread-safe streaming UI updates, the project illustrates how complex agentic pipelines can run reliably across both cloud endpoints and local LLM runtimes (like Ollama and llama.cpp).
Thanks for reading ⚡
Links
- Github repository for this post: https://github.com/aairom/deepagents-test-usecase
- Deep Agents repository: https://github.com/langchain-ai/deepagents
- Deep Agents by LangChain: https://www.langchain.com/deep-agents
- Deep Agents Documentation: https://docs.langchain.com/oss/python/deepagents/overview
- watsonx Orchestrate: https://www.ibm.com/products/watsonx-orchestrate
- IBM Bob: https://bob.ibm.com/







Top comments (0)