TL;DR:
agent_core.pyis the sole SDK coupling point, configuring 7 tools + ReAct loop + HITL gate all in one file.
Table of contents
Open Table of contents
Architecture: Single Coupling Point
DeepCoWork isolates all Deep Agents SDK interaction into agent_core.py. The export contract has five items:
| Export | Purpose |
|---|---|
build_llm() | Create LLM instance from settings |
build_agent() | Create DeepAgent instance |
stream_events() | SSE streaming |
get_agent_state() | Query agent state |
resume_agent_input() | Resume from HITL |
When the SDK version changes, only this file needs updating. The Deep Agents SDK API reference and LangGraph create_react_agent docs are the key references.
Dissecting create_deep_agent
The build_agent() function calls the SDK’s create_deep_agent:
from deepagents import create_deep_agent
from deepagents.backends import LocalShellBackend
def build_agent(
workspace_dir: Path,
checkpointer: Any,
mode: str = "cowork",
thread_id: str | None = None,
with_hitl: bool = True,
tools: list | None = None,
system_prompt: str | None = None,
) -> Any:
llm = build_llm()
backend = LocalShellBackend(
root_dir=str(workspace_dir),
virtual_mode=False,
timeout=60,
max_output_bytes=50_000,
inherit_env=True,
)
if system_prompt is None:
system_prompt = build_system_prompt(mode, workspace_dir)
interrupt_on: dict = (
{"write_file": True, "edit_file": True, "execute": True}
if with_hitl else {}
)
return create_deep_agent(
model=llm,
tools=tools or [],
backend=backend,
interrupt_on=interrupt_on,
checkpointer=checkpointer,
system_prompt=system_prompt,
skills=_resolve_skills(workspace_dir) or None,
)
Let’s examine each key parameter.
LocalShellBackend
LocalShellBackend gives the agent filesystem and shell access. Auto-provided tools:
| Tool | Type | Needs HITL |
|---|---|---|
read_file | Read | No |
write_file | Write | Yes |
edit_file | Write | Yes |
execute | Shell | Yes |
ls | Read | No |
glob | Read | No |
grep | Read | No |
Key configuration:
backend = LocalShellBackend(
root_dir=str(workspace_dir), # Sandbox boundary
virtual_mode=False, # Real filesystem
timeout=60, # Shell command timeout (seconds)
max_output_bytes=50_000, # Output size limit
inherit_env=True, # Inherit environment variables
)
root_dir is the agent’s sandbox boundary. No file access is possible outside this directory.
interrupt_on: The HITL Gate
The interrupt_on dictionary decides which tool calls pause execution:
interrupt_on = {"write_file": True, "edit_file": True, "execute": True}
When these tools are invoked, LangGraph suspends graph execution and sends an approval request to the frontend. On approval, execution resumes with Command(resume={"decisions": [...]}):
def resume_agent_input(decisions: list[dict]) -> Any:
return Command(resume={"decisions": decisions})
Sub-agents are created with with_hitl=False and run without HITL — the main agent already has approval.
The ReAct Loop
The core of DeepAgents SDK is a LangGraph-based ReAct (Reasoning + Acting) loop:
[User message]
|
v
LLM Reasoning
|
+-- Tool call decision (Acting)
| |
| v
| [interrupt_on check]
| |
| +-- HITL needed -> pause -> wait for approval
| +-- HITL not needed -> execute immediately
| |
| v
| Tool execution result
| |
+-------+
|
v
LLM Reasoning (decide next action)
|
+-- Complete -> final response
+-- Incomplete -> loop again
The stream_events() function converts this loop into SSE:
async for event in agent.astream(
agent_input,
stream_mode=["updates", "messages"],
subgraphs=True,
config=cfg,
):
stream_mode=["updates", "messages"] receives both tool call events and text tokens. subgraphs=True captures sub-agent events in ACP mode.
Checkpointer: State Persistence
AsyncSqliteSaver persists agent state to SQLite:
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
self.db_conn = await aiosqlite.connect(str(config.DB_PATH))
await self.db_conn.execute("PRAGMA journal_mode=WAL")
self.checkpointer = AsyncSqliteSaver(self.db_conn)
WAL (Write-Ahead Logging) mode ensures concurrent read/write performance. Conversation history and agent state survive app restarts.
Skill Resolution
_resolve_skills() scans global and workspace skill directories:
def _resolve_skills(workspace_dir: Path) -> list[str]:
sources: list[str] = []
global_skills = config.WORKSPACE_ROOT / "skills"
if global_skills.is_dir():
sources.append("skills/")
ws_skills = workspace_dir / "skills"
if ws_skills.is_dir():
sources.append("skills/")
return sources
Priority: global (~/.cowork/skills/) < workspace ({workspace}/skills/). Later-loaded skills take precedence.
Benchmark
| Metric | Value |
|---|---|
| LocalShellBackend tool count | 7 (read_file, write_file, edit_file, execute, ls, glob, grep) |
| Custom tool count (web_search, memory, task, etc.) | 4 |
| Typical Cowork task ReAct loop iterations | 8-15 |
| MAX_AGENT_ITERATIONS setting | 25 |
| Checkpointer SQLite WAL read performance | ~0.3ms/query |
FAQ
Can it work without the Deep Agents SDK?
No. create_deep_agent and LocalShellBackend are core dependencies. However, by replacing only agent_core.py, you could swap in a different agent framework.
What happens with virtual_mode=True?
File writes are handled in memory without touching disk. Useful for testing or preview, but DeepCoWork uses False since real filesystem changes are the goal.
Why max_output_bytes=50_000?
Large shell outputs consume LLM context. The 50KB limit prevents commands like npm install from overwhelming the agent with verbose output.
Series
- DeepCoWork: I Built an AI Agent Desktop App
- Tauri 2 + Python Sidecar
- [This post] DeepAgents SDK Internals
- System Prompt Design per Mode
- SSE Streaming Pipeline
- HITL Approval Flow
- Multi-Agent ACP Mode
- Agent Memory 4 Layers
- Skills System
- LLM Provider Integration
- Security Checklist
- GitHub Actions Cross-Platform Build