Rebuilding a Claude Code-Style Agent With LangChain Deep Agents
How a coding agent's harness works, from the core loop to tools, planning, memory, subagents and sandboxed approvals, rebuilt step by step with deepagents.
This article is published in English.
Sources: **Build Your Own Claude Code Using Langchin: A Deepdive Into LangChain’s Deep Agents**
A coding agent isn't a chatbot
It's easy to picture a coding agent as a smarter chatbot, and that picture is wrong. Claude Code's capability doesn't come from the model alone. It comes from the agentic harness built around the model. The harness is the control plane: the unseen system that gives the model tools for reading files, an environment for running code, memory of what it already did and a checklist to stay on course. Take the harness away and a language model is a text generator. Put it back and the same model behaves like a capable, autonomous software engineer.
The plan here is to look at how a Claude Code-style agent works, piece by piece, and to rebuild each piece with LangChain's deepagents library. By the end the anatomy should make sense because you'll have built one.
The loop at the center
A session opens with the user's message. It might be a throwaway question about what's in a folder or a big job like reworking the login system and updating its tests. The model reads that message before choosing a move, and from there the path forks. When action is needed it requests a tool, gets the output back and circles around again, and this cycle is identical for a one-step task and a fifty-step one. Once it replies in ordinary prose with no tool request, the loop breaks and the user sees the result. The absence of a tool call is exactly the signal that the agent considers itself done.
The "decides the next step" box blends three phases that research on agents describes: gathering context by reading files, acting by editing and running commands, and verifying by running tests. They don't run as separate modes. They're simply different tool calls flowing through one loop.
There's one intentional change of gears, plan mode. Usually the agent plans and acts together. In plan mode a gate sits ahead of the loop. The agent investigates and drafts a proposal while changing nothing: files get read and ideas get worked out, but edits and commands wait until the user signs off, and only then does the normal loop run. That makes it a safety valve for big or risky jobs. It isn't the same as the to-do list the agent keeps for itself on every task, because plan mode is a pause the user triggers and must explicitly approve.
The diagram still omits the machinery wrapped around the loop: compaction when memory fills, subagents running private loops of their own, and the permission and sandbox layer standing between "run a tool" and the real system. None of those alter the loop. They surround it.
Four capabilities sit inside the harness, and they're what keep the loop steady. Planning has the agent think first. Tools act as its hands. Context management stops long jobs from swamping its memory. Subagents take side tasks into private loops of their own so the main thread stays uncluttered. They supply the loop and never rewrite it.
Underneath all of that is the layer that matters most. When the agent wants to touch the real world by editing a file, running a command or using the network, the action doesn't reach the machine directly. It first goes through permissions and a sandbox, and no other safety idea in the design carries as much weight. Imagine trying to keep an agent safe by adding a line like "please don't delete anything important" to its instructions. That's a sticky note, and a model can overlook it, misunderstand it or be argued out of it. Protection that holds sits outside the model: a permission rule that flatly rejects a tool call, and an operating-system sandbox that halts a dangerous command no matter what the model prefers.
The bare engine
Before reaching for a library, it helps to see the engine with nothing on it. This is a complete, working agent loop.
def run_agent_loop(client, user_message, tools, tool_functions, max_turns=20):
messages = [{"role": "user", "content": user_message}]
for _ in range(max_turns):
# 1. Ask the model what to do next.
response = client.messages.create(
model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=messages
)
messages = [*messages, {"role": "assistant", "content": response.content}]
# 2. Plain text and no tool request? The job is done.
tool_uses = [b for b in response.content if b.type == "tool_use"]
if not tool_uses:
return "".join(b.text for b in response.content if b.type == "text")
# 3. Run each requested tool and hand the results back. 4. Repeat.
results = [
{"type": "tool_result", "tool_use_id": call.id,
"content": tool_functions[call.name](**call.input)}
for call in tool_uses
]
messages = [*messages, {"role": "user", "content": results}]
raise RuntimeError(f"agent loop did not finish within {max_turns} turns")
It comes to roughly thirty lines, and the rest of the design is this loop with extras attached. Stronger tools drop into tool_functions. A planner jots notes into messages. A compactor trims messages once it swells. A subagent is just the same function called again with a new, empty messages list. A permission check goes in the gap between the model requesting a tool and the line that executes it. The max_turns cap is the bit of paranoia that every real harness keeps, because a model stuck in a loop spends money without end.
Every coding agent has this beating heart, the agent loop that turns talking into doing, and it's almost embarrassingly simple:
- Ask the model what to do next.
- The model either answers in plain text or asks to use a tool.
- If it asked for a tool, the system runs the tool and returns the result to the model.
- Go back to step 1.
- When the model answers in plain text with no tool request, the job is done, so stop and show the answer.
Reading a file, running a command and editing code all pass through that one cycle. Picture a contractor given a job and a checklist: check what's next, do it, inspect the outcome, choose again, and only report back once the list is empty. The loop plays the role of that contractor's judgment. It also scales itself, since a simple question may need a single turn while a sweeping refactor chains dozens of tool calls, and no one has to fix the step count ahead of time.
Building it with Deep Agents
There's no need to write the while loop by hand. Creating a Deep Agent provides a working loop automatically. Start by installing the library and a model provider.
pip install deepagents langchain-anthropic
Then create the simplest possible agent.
from deepagents import create_deep_agent
def get_weather(city: str) -> str:
"""Get the weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
tools=[get_weather],
system_prompt="You are a helpful assistant.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What's the weather in San Francisco?"}]}
)
That create_deep_agent call returns an agent with the loop already wired in, and .invoke() runs exactly the cycle described above. The harness doesn't care which model sits underneath, whether OpenAI, Google or Anthropic. The loop stays the same.
Tools are the hands
Where the loop is the engine, tools are the hands. With none, a model can only talk. With good ones it can read a codebase, alter it and verify the result. Real coding tools come in four families:
- Reading tools look around without changing anything:
read_fileopens a file,globfinds files by name pattern such as every.pyfile, andgrepsearches inside files for specific text. Beginners often confuse the last two, but glob finds files by name and grep finds them by content. - Editing tools change things safely:
write_filecreates new files andedit_filemodifies part of an existing one. - An execute tool runs shell commands, the same ones typed in a terminal.
- A delegate tool,
task, hands a sub-task to a helper agent.
Why not let the agent use cat for reading and sed for editing? It could, but purpose-built tools win on two counts. One is token budgeting: a dedicated read tool can look at a file's size before loading it and cut it down if necessary, while raw cat dumps the whole thing and can blow past the context window. The other is tidier permissions: a call to the known edit_file tool tells the harness exactly what's going on so it can apply safety rules, but a free-form shell command is an opaque and risky box.
There's an amusing quirk here. Models learned from a public internet full of Stack Overflow answers that use cat and sed, so even advanced ones reach for raw shell out of habit. The system prompt has to remind them firmly to use their real tools and not the shell shortcuts.
Deep Agents ships the full set built in: planning, filesystem tools, execute and delegation. Custom tools go on top as decorated functions.
import subprocess
from langchain_core.tools import tool
MAX_OUTPUT_CHARS = 20_000 # roughly 5k tokens
@tool
def run_tests(path: str = ".") -> str:
"""Run the project's pytest suite and return its output.
Output is truncated to the last 20k characters (failures appear at the
end). The run is killed after 5 minutes.
"""
try:
result = subprocess.run(["pytest", path], capture_output=True,
text=True, check=False, timeout=300)
except subprocess.TimeoutExpired:
return "pytest timed out after 300s"
output = result.stdout + result.stderr
if len(output) > MAX_OUTPUT_CHARS:
return ("[... output truncated to the last 20,000 characters ...]\n"
+ output[-MAX_OUTPUT_CHARS:])
return output
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
tools=[run_tests], # merged in alongside the built-ins
system_prompt="You are a coding assistant. Always run tests after editing.",
)
The @tool decorator makes an ordinary function callable by the model. Its docstring carries real weight, since the model treats it as the manual that says when and how to use the tool.
Planning before acting
Give a complex request to a bare model and it often thrashes: a call here, another there, no overall plan, occasionally running in circles. Good coding agents sidestep that by drawing up a plan before they touch anything. They write a structured to-do list that splits the job into clear steps, then work down it and mark each item in progress or done. It's like a cook who reads the entire recipe before turning on the burner, so step four isn't being invented while step three scorches.
There's a neat engineering trick underneath. Over a long session the original goal gets buried beneath pages of tool output, and the model loses sight of it. To counter that, the harness slips the current to-do list back into the conversation after tool calls, the way a coworker might slide the checklist back across the desk as a reminder.
In Deep Agents this is the built-in write_todos tool. Nothing needs configuring, because the default system prompt teaches the agent to plan before acting and to keep the list updated across the loop.
Context management
The toughest engineering sits here. Each model has a context window, a fixed ceiling on its working memory. A handful of big files plus a few commands fill it quickly, and once it overflows the agent starts dropping things, even the instructions it began with. Coding agents handle this with two tactics:
- Files serve as outside memory. Instead of pushing a huge search result into the chat, the agent writes it to disk and keeps just the file name, opening the file again when it needs the detail.
- Compaction triggers as the conversation approaches the limit. The harness stops the loop, condenses the older exchanges, parks the key facts in long-term storage and wipes the swollen short-term memory.
Think of writing a massive report: nobody holds every book in their head. You take notes, file the books and keep a one-page summary on the desk. Both halves come built into Deep Agents, which provides a virtual filesystem for offloading large results plus automatic summarization middleware, so this memory management is there from the moment the agent is created.
Subagents: divide and conquer
Some tasks are too large for one conversation. Searching a huge codebase might mean reading fifty files, and fifty files shouldn't clog the main agent's clean memory. The answer is subagents. The main agent starts a helper, gives it one narrow job such as finding where the auth logic lives, and the helper works in its own separate blank context window. When finished, it returns a concise summary. A manager who delegates research to a junior employee and receives a one-paragraph summary keeps a clean desk in the same way.
There's a safety rail too: subagents are strictly forbidden from starting subagents of their own. Otherwise a confused agent could spawn an endless chain of nested helpers, a runaway process burning through the API budget. A strict depth limit keeps delegation safe.
Deep Agents includes a built-in task tool for spawning helpers. Specialized ones, like a dedicated code searcher, are described with simple dictionaries.
code_searcher = {
"name": "code-searcher",
"description": "Searches the codebase to find where specific logic lives. "
"Use this for any open-ended 'where is X?' question.",
# ⚠️ NOTE: the key is `system_prompt`, NOT `prompt` (see correction below).
"system_prompt": "You are an expert at navigating codebases. Use the grep "
"and glob tools to locate relevant files, then report a "
"concise summary of what you found and where. Do not make "
"any edits.",
}
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
tools=[run_tests],
system_prompt="You are a coding assistant.",
subagents=[code_searcher],
)
The main agent now knows a specialist exists, which leaves its own memory clean for the real coding work.
Safety and human approval
At this stage the agent can edit files and run shell commands on a real machine, which deserves a moment's thought. An enthusiastic agent might remove the wrong files or run something destructive. Strength with no brakes is a real liability. Coding agents put controls in two layers:
- Allowlists and deny rules let safe tools, such as reading files, run automatically while destructive ones, such as deleting files, need approval.
- Approval prompts, also called human-in-the-loop, make the agent stop and ask before risky actions. The user approves, edits or rejects the action, and then the loop continues.
The core rule of agent security is that a prompt can't be a security boundary. Instructing a model not to delete anything important is a courteous request and not a barrier, because the model can hallucinate straight past it. Enforcement has to live outside the model, in the harness. A permission rule can refuse a tool call, and a sandbox can block a hazardous shell command at the operating-system level however much the model wants it to run. For that reason the execute tool needs a proper backend. Politeness isn't security.
Deep Agents puts its limits in the harness. Shell access requires a backend, and LangGraph's built-in interrupt mechanism pauses the run ahead of risky operations.
from deepagents import create_deep_agent
# Import path for backends can vary by version - check the current
# "Backends" page in the Deep Agents docs.
from deepagents.backends import LocalShellBackend
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
system_prompt="You are a coding assistant working inside this project.",
backend=LocalShellBackend(), # enables the `execute` shell tool
)
If a gated tool gets requested, the loop halts and the result includes an __interrupt__ payload that lays out the pending action and the choices on offer: approve, edit, reject or respond. After the decision, the run resumes.
from langgraph.types import Command
result = agent.invoke({"messages": [...]}, config)
result["__interrupt__"] # the pending action + allowed decisions — show your user
# You decide; the loop picks up exactly where it paused:
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config)
# ... or {"type": "reject"} — the tool is never run, and the model is told so.
Real limits belong in the harness, never only in the prompt.
Memory across sessions
Out of the box, an agent loses everything as soon as a conversation closes. A genuinely useful assistant ought to remember a project's coding conventions, what the user likes and where it left off yesterday. Two mechanisms do that job. Checkpointing stores the agent's precise state, so a long task survives an interruption and carries on from the same point. Long-term memory holds facts that outlast any one conversation, for instance that a project indents with four spaces.
Since Deep Agents runs on LangGraph, persistence comes from plugging in a checkpointer.
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import InMemorySaver
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
tools=[run_tests],
system_prompt="You are a coding assistant.",
checkpointer=InMemorySaver(), # remembers state within a session
)
# A "thread_id" ties messages together into one ongoing conversation.
config = {"configurable": {"thread_id": "project-alpha"}}
agent.invoke(
{"messages": [{"role": "user", "content": "Start refactoring the auth module."}]},
config=config,
)
# Later, same thread_id - the agent remembers the earlier turn:
agent.invoke(
{"messages": [{"role": "user", "content": "Now update the tests too."}]},
config=config,
)
InMemorySaver lasts only for the life of the program. For persistence that survives reboots, swap it for a database-backed checkpointer.
Everything in one agent
Assembled together, the pieces form one agent: a core loop plus custom tools, planning, context handling, a helper subagent, a shell and saved state, seven parts in all.
from deepagents import create_deep_agent
from deepagents.backends import LocalShellBackend
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
# --- A custom tool (Part 2) — token-budgeted, see Part 2 for the full body ---
@tool
def run_tests(path: str = ".") -> str:
"""Run the project's pytest suite and return its (truncated) output."""
import subprocess
try:
result = subprocess.run(["pytest", path], capture_output=True,
text=True, check=False, timeout=300)
except subprocess.TimeoutExpired:
return "pytest timed out after 300s"
output = result.stdout + result.stderr
return output if len(output) <= 20_000 else "[... truncated ...]\n" + output[-20_000:]
# --- A specialized subagent (Part 5) — note the `system_prompt` key ---
code_searcher = {
"name": "code-searcher",
"description": "Finds where specific logic lives in the codebase. "
"Use for open-ended 'where is X?' questions.",
"system_prompt": "You navigate codebases using grep and glob, then report a "
"concise summary of what you found. You never make edits.",
}
# --- A system prompt that teaches good behavior (Parts 2 & 3) ---
SYSTEM_PROMPT = """You are a careful coding assistant.
Workflow:
1. Plan the task as a to-do list before doing anything.
2. Use your built-in read, grep, and glob tools to explore - never the raw shell
equivalents like cat or grep.
3. Make focused edits.
4. ALWAYS run the tests after editing, and fix anything that breaks.
5. Delegate broad codebase searches to the code-searcher subagent.
"""
# --- Assemble the agent (Parts 1, 4, 6, 7 handled by the harness) ---
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6", # Part 1: the loop, model-agnostic
tools=[run_tests], # Part 2: custom hands
system_prompt=SYSTEM_PROMPT, # Parts 2 & 3: behavior + planning
subagents=[code_searcher], # Part 5: delegation
backend=LocalShellBackend(root_dir=".", virtual_mode=False), # Part 6: shell access
interrupt_on={"execute": True, # Part 6: the brakes —
"write_file": True, "edit_file": True}, # approval before anything destructive
checkpointer=InMemorySaver(), # Part 7: memory across turns
)
# Part 4 (context management) and built-in planning come on automatically.
config = {"configurable": {"thread_id": "my-project"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "The login tests are failing. Fix them."}]},
config=config,
)
print(result["messages"][-1].content)
Fewer than a hundred lines produce a coding agent that plans, searches, edits, runs tests, hands work to helpers, stays inside a sandbox and keeps hold of its context. Its anatomy matches Claude Code's: a plain loop wrapped in tools, planning, memory handling, delegation and brakes that sit outside the model.