LangChain DeepAgents
Source studied:
libs/deepagents/deepagents/**(the SDK core), with cross-references to sibling packages underlibs/{cli,acp,code,repl,evals,partners/*}. Library version: seelibs/deepagents/deepagents/_version.py.
This report explains how the DeepAgents Python SDK is organized, the OOP idioms it uses, and the role of each layer. It is written for a reader who already understands LangChain / LangGraph at a high level, and wants a map of the codebase before diving in.
1. Repository topology (monorepo)
Section titled “1. Repository topology (monorepo)”DeepAgents is a Python monorepo of independently versioned packages:
deepagents/ (repo root)├── libs/│ ├── deepagents/ ← SDK core (focus of this report)│ ├── cli/ ← interactive CLI on top of the SDK│ ├── acp/ ← Agent Context Protocol server│ ├── code/ ← placeholder for future "code" capabilities│ ├── repl/ ← language-agnostic REPL middleware (langchain_repl)│ ├── evals/ ← eval suite + Harbor integration│ └── partners/│ ├── daytona/ ← DaytonaSandbox (BaseSandbox subclass)│ ├── modal/ ← ModalSandbox (BaseSandbox subclass)│ ├── runloop/ ← RunloopSandbox (BaseSandbox subclass)│ └── quickjs/ ← QuickJS REPL middleware└── examples/ ← reference agents (research, content writer, etc.)The dependency direction is inward:
partners/* cli/ acp/ evals/ examples/ \ | | | / \ v v v / +─────────────► libs/deepagents ◄───────────+ │ langchain · langgraph · langchain_anthropicEverything outside libs/deepagents/ depends on the SDK; the SDK depends only on LangChain / LangGraph and a small number of provider integrations.
2. The SDK package layout
Section titled “2. The SDK package layout”libs/deepagents/deepagents/├── __init__.py ← public surface├── graph.py ← create_deep_agent (factory function)├── _models.py ← resolve_model() helper├── _tools.py ← tool description rewriting├── _excluded_middleware.py ← profile-driven middleware filtering├── _api/deprecation.py ← @deprecated decorator├── backends/ ← pluggable storage / execution│ ├── protocol.py ← BackendProtocol, SandboxBackendProtocol│ ├── sandbox.py ← BaseSandbox (template-method ABC)│ ├── state.py ← StateBackend│ ├── filesystem.py ← FilesystemBackend│ ├── store.py ← StoreBackend│ ├── composite.py ← CompositeBackend (route by prefix)│ ├── local_shell.py ← LocalShellBackend│ └── langsmith.py ← LangSmithSandbox├── middleware/ ← cross-cutting AgentMiddleware classes│ ├── filesystem.py ← FilesystemMiddleware│ ├── subagents.py ← SubAgentMiddleware (+ task tool)│ ├── async_subagents.py ← AsyncSubAgentMiddleware│ ├── memory.py ← MemoryMiddleware│ ├── skills.py ← SkillsMiddleware│ ├── summarization.py ← _DeepAgentsSummarizationMiddleware│ ├── permissions.py ← FilesystemPermission helpers│ ├── patch_tool_calls.py ← PatchToolCallsMiddleware│ └── _tool_exclusion.py ← _ToolExclusionMiddleware└── profiles/ ← config / registry of provider+harness profiles ├── provider/ ← ProviderProfile (model-construction concerns) └── harness/ ← HarnessProfile (runtime behavior) └── harness/_*.py ← built-ins for Anthropic, OpenAI Codex, …The four salient concerns the SDK separates are:
| Concern | Lives in | OOP shape |
|---|---|---|
| Build a chat model | _models.py | function + ProviderProfile registry |
| Where files / shell live | backends/ | ABC hierarchy (Strategy + Template Method) |
| What the agent loop does | middleware/ | AgentMiddleware subclasses (Decorator/Pipeline) |
| Per-model tweaks of the above | profiles/ | Frozen dataclass value objects + registry |
| Stitching it all together | graph.py | factory function (composition root) |
3. The composition root: create_deep_agent
Section titled “3. The composition root: create_deep_agent”create_deep_agent(...) (graph.py:203) is the only public constructor. It is not a class; it is a factory that:
- Resolves
model→BaseChatModelviaresolve_model(consultsProviderProfileregistry). - Looks up the matching
HarnessProfile(per-model configuration overlay). - Validates and processes
subagents(each becomes a sub-graph, optionally with its own middleware stack). - Builds the main agent’s middleware stack in a deterministic order.
- Filters out any
excluded_middlewarefrom the harness profile. - Assembles the final system prompt (user → BASE/CUSTOM → SUFFIX).
- Delegates to
langchain.agents.create_agent(...)to compile aCompiledStateGraph.
The middleware stack assembled looks like this:
┌── Base stack (always in this order) ──────────────────────────────┐│ TodoListMiddleware ││ SkillsMiddleware? (if `skills=` provided) ││ FilesystemMiddleware (REQUIRED — cannot be excluded) ││ SubAgentMiddleware? (if any sync subagents) (REQUIRED) ││ SummarizationMiddleware ││ PatchToolCallsMiddleware ││ AsyncSubAgentMiddleware? (if async subagents) │├── User middleware (caller-supplied) ──────────────────────────────┤│ <user middleware...> │├── Tail stack ─────────────────────────────────────────────────────┤│ HarnessProfile.extra_middleware ││ _ToolExclusionMiddleware? (if profile lists excluded_tools) ││ AnthropicPromptCachingMiddleware (no-op for non-Anthropic) ││ MemoryMiddleware? (if `memory=` provided) ││ HumanInTheLoopMiddleware? (if `interrupt_on=` provided) │└───────────────────────────────────────────────────────────────────┘Two classes — FilesystemMiddleware and SubAgentMiddleware — are protected scaffolding (_REQUIRED_MIDDLEWARE in graph.py:173). Any attempt to remove them via HarnessProfile.excluded_middleware raises ValueError. This is the SDK’s structural integrity guarantee.
4. OOP architecture overview
Section titled “4. OOP architecture overview”The SDK uses three small class hierarchies that all hang off LangChain base types:
- Backends (storage / shell execution) — rooted at
BackendProtocol(an ABC). - Middleware (cross-cutting concerns) — all subclass
langchain.agents.middleware.types.AgentMiddleware. - Profiles (per-model configuration) — frozen
@dataclassvalue objects living in registries.
Subagents (SubAgent, CompiledSubAgent, AsyncSubAgent) are intentionally TypedDicts, not classes — they are data specs that the middleware turns into runnables.
4.1 High-level component diagram
Section titled “4.1 High-level component diagram”flowchart TB
User["User code"]
Factory["create_deep_agent()<br/>(graph.py)"]
LCAgent["langchain.agents.create_agent()"]
Graph["CompiledStateGraph"]
subgraph Profiles["profiles/ (configuration)"]
PP["ProviderProfile<br/>(model construction)"]
HP["HarnessProfile<br/>(runtime behavior)"]
GP["GeneralPurposeSubagentProfile"]
end
subgraph Backends["backends/ (storage + execution)"]
BP["BackendProtocol (ABC)"]
SBP["SandboxBackendProtocol"]
BS["BaseSandbox"]
SB["StateBackend"]
FB["FilesystemBackend"]
ST["StoreBackend"]
CB["CompositeBackend"]
LSB["LocalShellBackend"]
LSS["LangSmithSandbox"]
Partner["DaytonaSandbox<br/>ModalSandbox<br/>RunloopSandbox"]
end
subgraph Middleware["middleware/ (cross-cutting)"]
FM["FilesystemMiddleware"]
SM["SubAgentMiddleware"]
ASM["AsyncSubAgentMiddleware"]
MM["MemoryMiddleware"]
SkM["SkillsMiddleware"]
SuM["SummarizationMiddleware"]
PM["PatchToolCallsMiddleware"]
TE["_ToolExclusionMiddleware"]
end
User -->|"model, tools, subagents,<br/>skills, memory, ..."| Factory
Factory -->|"resolve_model"| PP
Factory -->|"_harness_profile_for_model"| HP
HP --> GP
Factory --> Backends
Factory -->|builds stack| Middleware
Middleware -.->|"backend ="| BP
Factory --> LCAgent
LCAgent --> Graph
4.2 Backend hierarchy (Strategy + Template Method)
Section titled “4.2 Backend hierarchy (Strategy + Template Method)”classDiagram
class BackendProtocol {
<<abstract>>
+ls(path) LsResult
+read(file_path, offset, limit) ReadResult
+write(file_path, content) WriteResult
+edit(file_path, old, new, replace_all) EditResult
+grep(pattern, path, glob) GrepResult
+glob(pattern, path) GlobResult
+upload_files(files) list~FileUploadResponse~
+download_files(paths) list~FileDownloadResponse~
+als/aread/awrite/aedit/agrep/aglob (async)
}
class SandboxBackendProtocol {
<<abstract>>
+id : str
+execute(command, timeout) ExecuteResponse
+aexecute(command, timeout) ExecuteResponse
}
class BaseSandbox {
<<abstract>>
+execute()* abstract
+upload_files()* abstract
+download_files()* abstract
+id* abstract
# ls/read/write/edit/grep/glob<br/>derived from execute() + upload_files()
- _edit_inline()
- _edit_via_upload()
- _write_preflight()
}
class StateBackend {
- _file_format : FileFormat
+ls/read/write/edit/...
}
class FilesystemBackend {
+ls/read/write/edit/... (real FS)
}
class StoreBackend {
- namespace_factory
+ls/read/write/edit/... (LangGraph BaseStore)
}
class CompositeBackend {
- default : BackendProtocol
- routes : dict~str, BackendProtocol~
+routes by path prefix
}
class LocalShellBackend {
+execute() unrestricted host shell
}
class LangSmithSandbox {
- _sandbox : Sandbox
+execute()
+upload_files()
+download_files()
}
class DaytonaSandbox
class ModalSandbox
class RunloopSandbox
BackendProtocol <|-- SandboxBackendProtocol
BackendProtocol <|-- StateBackend
BackendProtocol <|-- FilesystemBackend
BackendProtocol <|-- StoreBackend
BackendProtocol <|-- CompositeBackend
SandboxBackendProtocol <|.. BaseSandbox
BaseSandbox <|-- LangSmithSandbox
BaseSandbox <|-- DaytonaSandbox
BaseSandbox <|-- ModalSandbox
BaseSandbox <|-- RunloopSandbox
FilesystemBackend <|-- LocalShellBackend
SandboxBackendProtocol <|.. LocalShellBackend
Patterns at work
- Strategy pattern.
BackendProtocoldefines a family of interchangeable file-storage strategies; middleware (FilesystemMiddleware) holds a reference to one and is agnostic of which strategy is in use. - Template Method.
BaseSandbox(backends/sandbox.py) implementsls/read/write/edit/grep/globby delegating to the abstract primitivesexecute(),upload_files(), anddownload_files(). Concrete sandboxes (LangSmith, Daytona, Modal, Runloop) only implement those three plus anidproperty — everything else is inherited. - Composite pattern.
CompositeBackendroutes operations to different child backends by path prefix (e.g./memories/→StoreBackend, default →StateBackend). - Result objects (Outcome / Either). Operations return dataclasses (
ReadResult,WriteResult,EditResult,LsResult,GrepResult,GlobResult) with optionalerror: str | None. This avoids exceptions across the LLM/tool boundary, which is critical because tool failures must be returned as text to the model. - Backwards-compatibility deprecations.
BackendProtocol.ls,glob,grepkeep the oldls_info,glob_info,grep_rawmethods as@deprecatedshims; the protocol uses runtime introspection (type(self).ls_info is not BackendProtocol.ls_info) to detect subclasses still on the legacy API.
4.3 Middleware hierarchy (Decorator / Pipeline)
Section titled “4.3 Middleware hierarchy (Decorator / Pipeline)”classDiagram
class AgentMiddleware~StateT, ContextT, ResponseT~ {
<<abstract, from langchain>>
+tools : list~BaseTool~
+system_prompt : str | None
+modify_model_request()
+before_model() / after_model()
+modify_tool_call_request()
}
class FilesystemMiddleware {
+backend : BackendProtocol
+tools = [ls, read_file, write_file,<br/>edit_file, glob, grep, execute?]
- _permissions : list~FilesystemPermission~
- tool_token_limit_before_evict : int
}
class SubAgentMiddleware {
- _backend : BackendProtocol
- _subagents : list~SubAgent | CompiledSubAgent~
+tools = [task]
}
class AsyncSubAgentMiddleware {
- _async_subagents : list~AsyncSubAgent~
- _client_cache : _ClientCache
+tools = [start/check/update/cancel/list async tasks]
}
class MemoryMiddleware {
- _sources : list~str~
- _backend : BackendProtocol
- _add_cache_control : bool
}
class SkillsMiddleware {
- _sources : list~str~
- _backend : BackendProtocol
}
class _DeepAgentsSummarizationMiddleware
class SummarizationToolMiddleware
class PatchToolCallsMiddleware
class _ToolExclusionMiddleware
class HumanInTheLoopMiddleware {
<<from langchain>>
}
class AnthropicPromptCachingMiddleware {
<<from langchain_anthropic>>
}
AgentMiddleware <|-- FilesystemMiddleware
AgentMiddleware <|-- SubAgentMiddleware
AgentMiddleware <|-- AsyncSubAgentMiddleware
AgentMiddleware <|-- MemoryMiddleware
AgentMiddleware <|-- SkillsMiddleware
AgentMiddleware <|-- _DeepAgentsSummarizationMiddleware
AgentMiddleware <|-- SummarizationToolMiddleware
AgentMiddleware <|-- PatchToolCallsMiddleware
AgentMiddleware <|-- _ToolExclusionMiddleware
FilesystemMiddleware --> BackendProtocol : holds
SubAgentMiddleware --> BackendProtocol : holds
MemoryMiddleware --> BackendProtocol : holds
SkillsMiddleware --> BackendProtocol : holds
AgentMiddleware is generic in three type parameters (StateT, ContextT, ResponseT), so each middleware can extend the agent’s state schema (e.g. FilesystemState, MemoryState, SkillsState, AsyncSubAgentState). At graph compile time, LangChain merges these schemas into the agent’s effective AgentState. This is structural, additive state composition — middleware never overwrite each other’s keys.
Patterns at work
- Decorator / Pipeline. Each middleware wraps the agent loop with hooks (
before_model,after_model,modify_tool_call_request). Order matters and is fixed bycreate_deep_agent.AgentMiddlewareplays the role of a uniform decorator interface. - Tool injection. Middleware contributes tools by populating
self.tools = [...]in__init__. Thetasktool, for example, is manufactured bySubAgentMiddleware._build_task_toolfrom the registered subagent specs. - State extension via mixin-like generics. Each middleware that needs new state fields declares a
state_schema = MyState(a TypedDict subclass ofAgentState).
4.4 Profile hierarchy (frozen dataclass value objects)
Section titled “4.4 Profile hierarchy (frozen dataclass value objects)”classDiagram
class ProviderProfile {
<<frozen dataclass>>
+init_kwargs : Mapping
+pre_init : Callable | None
+init_kwargs_factory : Callable | None
+__post_init__() freezes mappings
}
class HarnessProfile {
<<frozen dataclass>>
+base_system_prompt : str | None
+system_prompt_suffix : str | None
+tool_description_overrides : Mapping
+excluded_tools : frozenset~str~
+excluded_middleware : frozenset
+extra_middleware : Sequence | factory
+general_purpose_subagent : GeneralPurposeSubagentProfile | None
+materialize_extra_middleware()
}
class HarnessProfileConfig {
<<frozen dataclass>>
+base_system_prompt : str | None
+system_prompt_suffix : str | None
+tool_description_overrides : Mapping
+excluded_tools : list~str~
+excluded_middleware : list~str~
+general_purpose_subagent : dict
+to_harness_profile() HarnessProfile
+from_dict(data) HarnessProfileConfig
}
class GeneralPurposeSubagentProfile {
<<frozen dataclass>>
+enabled : bool | None
+description : str | None
+system_prompt : str | None
+to_dict() / from_dict()
}
HarnessProfile *-- GeneralPurposeSubagentProfile : nested
HarnessProfileConfig ..> HarnessProfile : converts to
note for ProviderProfile "Registered by `provider` or<br/>`provider:model` key,<br/>consumed by resolve_model()"
note for HarnessProfile "Registered by `provider` or<br/>`provider:model` key,<br/>consumed by create_deep_agent()"
Patterns at work
- Value Object. All profile classes are
@dataclass(frozen=True)withMappingProxyTypedefenses on mutable fields. Equality, hashing, and “registry as a dict of profiles” all just work. - Registry pattern.
register_provider_profile()andregister_harness_profile()keep two module-level registries keyed byprovider:model. Built-ins are registered lazily on first access (Anthropic Sonnet 4.6, Opus 4.7, Haiku 4.5, OpenAI Codex, OpenAI Responses API defaults, OpenRouter app-attribution headers). - Profile merging. Provider profile lookup walks
provider:modelfirst, then falls back toprovider; both layers can register, and the more specific layer merges on top rather than replacing. The merge function preserves additive semantics forinit_kwargs_factory(both factories run, override wins on conflict) and forexcluded_*(set union). - Two-format serialization.
HarnessProfileis the runtime form;HarnessProfileConfigis the YAML/JSON-friendly subset (no class references, no callables).register_harness_profileaccepts either and converts.
4.5 Subagents — three flavors of TypedDict
Section titled “4.5 Subagents — three flavors of TypedDict”classDiagram
class SubAgent {
<<TypedDict>>
+name : str
+description : str
+system_prompt : str
+tools? : Sequence
+model? : str | BaseChatModel
+middleware? : list~AgentMiddleware~
+interrupt_on? : dict
+skills? : list~str~
+permissions? : list~FilesystemPermission~
+response_format? : ResponseFormat
}
class CompiledSubAgent {
<<TypedDict>>
+name : str
+description : str
+runnable : Runnable
}
class AsyncSubAgent {
<<TypedDict>>
+name : str
+description : str
+graph_id : str
+url? : str
+headers? : dict
}
class SubAgentMiddleware {
+tools = [task]
+_get_subagents()
+_build_task_tool()
}
class AsyncSubAgentMiddleware {
+tools = [start/check/update/cancel/list_async_tasks]
}
SubAgentMiddleware ..> SubAgent : compiles to runnable
SubAgentMiddleware ..> CompiledSubAgent : wraps as-is
AsyncSubAgentMiddleware ..> AsyncSubAgent : invokes remotely
SubAgentMiddleware._get_subagents() (middleware/subagents.py:550) is the adapter step that turns each SubAgent dict into a compiled LangChain agent (via create_agent) and each CompiledSubAgent into a passthrough runnable. The synthesized task tool dispatches by name to the correct runnable using a dict[str, Runnable] lookup. Async subagents follow the same pattern but invoke a remote LangSmith deployment instead of an in-process runnable.
5. Sequence: a single agent step
Section titled “5. Sequence: a single agent step”This is what happens when the user calls create_deep_agent(...) then agent.invoke({"messages": [...]}).
sequenceDiagram
actor User
participant CDA as create_deep_agent()
participant PP as ProviderProfile registry
participant HP as HarnessProfile registry
participant CA as langchain.create_agent()
participant Graph as CompiledStateGraph
participant FM as FilesystemMiddleware
participant SM as SubAgentMiddleware
participant Backend as BackendProtocol
participant Sub as Subagent runnable
participant Model as BaseChatModel
User->>CDA: create_deep_agent(model, tools, subagents, ...)
CDA->>PP: apply_provider_profile(model_spec)
PP-->>CDA: init_kwargs
CDA->>CA: init_chat_model(model, **kwargs)
CDA->>HP: _harness_profile_for_model(model)
HP-->>CDA: HarnessProfile
CDA->>CDA: build middleware stack<br/>(base + user + tail)
CDA->>CA: create_agent(model, prompt, tools, middleware)
CA-->>CDA: CompiledStateGraph
CDA-->>User: agent
User->>Graph: agent.invoke({"messages":[...]})
Graph->>FM: before_model hooks
FM->>Backend: ls/read/grep on demand
Graph->>Model: chat completion w/ tools
Model-->>Graph: AIMessage with tool_call("task", ...)
Graph->>SM: tool dispatch ("task")
SM->>Sub: subagent.invoke(state)
Sub->>Model: nested model calls
Sub-->>SM: final messages
SM-->>Graph: ToolMessage(content)
Graph-->>User: final state
Key observations:
- The main agent and its subagents share the same
BackendProtocolinstance (passed throughSubAgentMiddleware._backend), so files written by a subagent are visible to the parent. FilesystemMiddleware._permissionsis enforced at the tool layer, not the backend layer (seegraph.py:391docstring). This is documented as a current implementation detail that may move into the backend.- The
tasktool returns aCommandobject, which LangGraph interprets as both a state update and aToolMessagefor the parent’s transcript.
6. Sibling packages and how they extend the core
Section titled “6. Sibling packages and how they extend the core”The SDK is intentionally extension-friendly. Sibling packages reuse the public OOP surface:
| Package | Extends | What it adds |
|---|---|---|
libs/cli | AgentMiddleware | AskUserMiddleware, ConfigurableModelMiddleware |
libs/repl/langchain_repl | AgentMiddleware | ReplMiddleware + a stack-based Interpreter VM |
libs/acp/deepagents_acp | acp.ACPAgent | AgentServerACP — Agent Context Protocol server |
libs/partners/daytona | BaseSandbox | DaytonaSandbox — Daytona-backed sandbox |
libs/partners/modal | BaseSandbox | ModalSandbox — Modal-backed sandbox |
libs/partners/runloop | BaseSandbox | RunloopSandbox — Runloop-backed sandbox |
libs/partners/quickjs | AgentMiddleware | REPLMiddleware — sandboxed JS evaluation |
The takeaway: the SDK is “open for extension” along exactly two axes — new backends (subclass BaseSandbox or BackendProtocol) and new middleware (subclass AgentMiddleware). Almost every advanced feature in the wider repo is one of those two.
7. Cross-cutting OOP idioms (summary)
Section titled “7. Cross-cutting OOP idioms (summary)”┌─────────────────────────────────────────────────────────────────────────┐│ Idiom │ Where │├─────────────────────────────┼───────────────────────────────────────────┤│ Strategy │ BackendProtocol vs. concrete backends ││ Template Method │ BaseSandbox.{ls,read,...} via execute() ││ Composite │ CompositeBackend (route by prefix) ││ Decorator / Pipeline │ AgentMiddleware stack in create_deep_agent││ Adapter │ SubAgentMiddleware._get_subagents ││ Registry │ profiles/_keys.py + register_*_profile() ││ Value Object (frozen DC) │ HarnessProfile, ProviderProfile, ... ││ Result/Outcome │ ReadResult, WriteResult, EditResult, ... ││ TypedDict as DTO │ SubAgent, CompiledSubAgent, AsyncSubAgent ││ Factory function │ create_deep_agent (composition root) ││ Lazy registry init │ Built-in profiles loaded on first access ││ Deprecation shim │ _api/deprecation.py + protocol fallbacks │└─────────────────────────────────────────────────────────────────────────┘Two design choices stand out as deliberate and worth highlighting:
- The “agent” is not a class. It is an immutable
CompiledStateGraphproduced by a factory. Behaviors are composed by stackingAgentMiddlewareinstances, not by subclassing anAgentsuperclass. This avoids the diamond-inheritance / “god class” trap and matches LangGraph’s own data-oriented model. - Configuration is data, code is structure. The
profiles/package treats per-model differences (Anthropic vs OpenAI Responses vs OpenRouter vs Codex) as frozen dataclass values that get merged and passed in. This keeps the OOP graph small and avoids one-class-per-model proliferation.
8. Where to read next (suggested order)
Section titled “8. Where to read next (suggested order)”libs/deepagents/deepagents/graph.py— start here; the docstring oncreate_deep_agentis the canonical assembly description.backends/protocol.pythenbackends/sandbox.py— understand the Strategy + Template Method core.middleware/filesystem.pythenmiddleware/subagents.py— the two protected-scaffolding middlewares; everything else follows the same pattern.profiles/harness/harness_profiles.py— readHarnessProfile,HarnessProfileConfig, and_harness_profile_for_model. The merge logic is the model that other “registry of frozen value objects” code follows.libs/partners/modal/langchain_modal/sandbox.py— a 100-line concreteBaseSandboxis the cleanest worked example of the extension model.
Diagrams above are written in Mermaid. Render with any Mermaid-aware viewer (GitHub, VS Code “Markdown Preview Mermaid Support”, mermaid-cli, etc.).