Deep Agent
Deep Agent
A Deep Agent is a coding-style agent with direct access to the
filesystem and a shell, scoped to a single working directory. Where a
FunctionCallingAgent typically calls a few narrow tools (search,
calculate, fetch), a deep agent treats a workspace as its environment
and edits it in place — read files, grep them, write or patch them,
then run a command to validate the result.
What Deep Agents Can Do
graph LR
A[User Task] --> B[DeepAgent]
B --> C{Pick Tool}
C -->|Discover| D[list_directory]
C -->|Find/grep| E[search_files]
C -->|Read code| F[read_file]
C -->|Patch| G[edit_file]
C -->|Create| H[write_file]
C -->|Run| I[run_bash]
D --> B
E --> B
F --> B
G --> B
H --> B
I --> B
B --> J[Final Answer]
Typical use cases:
- Code review / explanation: read a repo, summarize what each file does.
- Bug fix loop: find the failing test, read the relevant module, patch it, re-run the test, iterate.
- Project bootstrapping: scaffold a small project from scratch
(write files, run
python -m pytestto confirm). - Data wrangling: search a directory of CSVs / logs, extract what's relevant, write a report.
The Six Built-in Tools
| Tool | Purpose |
|---|---|
list_directory |
Enumerate a directory (name, type, size). |
search_files |
Glob for files and/or grep their contents (regex). |
read_file |
Line-paginated file read, output prefixed with line numbers (cat -n style). |
write_file |
Create or overwrite a file. Gated by allow_write. |
edit_file |
Exact-string replacement; rejects 0 or 2+ occurrences. Gated by allow_write. |
run_bash |
Execute a shell command with timeout. Gated by allow_bash. |
Security Model
The deep agent enforces two different containment guarantees, and it's important to know which is real and which is not.
File tools (read_file / write_file / edit_file /
list_directory / search_files) refuse any path that resolves
outside the workdir, including:
..traversal (subdir/../../etc/passwd)- Absolute paths (
/etc/passwd) - Symlinks pointing outside the workdir (the symlink is resolved during the path check, so the target's location is what's compared)
Paths are canonicalized with Path.resolve() (which flattens ..
and follows existing symlinks) and then prefix-checked against the
resolved workdir. File opens use O_NOFOLLOW where the OS supports
it as defense in depth against TOCTOU symlink-swap races. This is a
robust boundary.
Bash is not sandboxed. The shell runs with cwd=workdir, but
that's just where its prompt starts — the LM can write cat
/etc/passwd and the shell will happily read it. The Python layer
cannot make run_bash safe on its own. If you're running this on
untrusted input, run the host process inside a container or other
OS-level isolation, or disable bash with allow_bash=False.
Building the Agent
DeepAgent mirrors FunctionCallingAgent — every parameter on that
class is accepted with identical semantics. The only additions are
workdir (required), the per-tool gates (allow_write, allow_bash),
and a few output-shaping knobs (timeout, max_output_chars,
max_search_results).
import synalinks
agent = synalinks.DeepAgent(
workdir="/tmp/my_project",
language_model=lm,
allow_write=True, # default
allow_bash=True, # default
timeout=30, # per-bash-command timeout, seconds
max_iterations=10, # coding tasks tend to need more rounds than RAG/SQL
)
Read-only mode for code review without write privileges:
agent = synalinks.DeepAgent(
workdir="/path/to/repo",
language_model=lm,
allow_write=False, # only read_file/list_directory/search_files (+ run_bash if allowed)
allow_bash=False, # purely static inspection
)
User-supplied extra tools (e.g. a date helper, a web search) are
passed via tools= and merged with the built-ins. The same name-
collision and leading-underscore rules apply as for every other
FunctionCallingAgent-derived class.
Example Usage
This example creates a small Python project, asks the agent to add a function, then verifies the addition by running a quick check.
result = await agent(ChatMessages(messages=[
ChatMessage(
role="user",
content=(
"Open the calculator.py file, add a `multiply(a, b)` "
"function next to `add`, and run `python -c 'from "
"calculator import multiply; print(multiply(6, 7))'` "
"to confirm it works."
),
)
]))
The agent will typically:
list_directory(".")to see what's in the workdir.read_file("calculator.py")to read existing code.edit_file(...)to insert the new function (orwrite_filefor a full rewrite if the file is short).run_bash("python -c '...'")to validate.- Stop and answer.
Key Takeaways
- One module, six tools:
synalinks.DeepAgentbundles file IO, search, and shell execution into a single ready-to-use agent. - Real path-traversal defense: file tools refuse anything outside the workdir, including symlink escapes.
- Bash is not sandboxed: containerize if you need true isolation.
- Read-only mode: set
allow_write=False(and optionallyallow_bash=False) for inspector agents that audit code without changing it. - Token-aware:
read_filereturns line-numbered output (LMs cite line numbers, no re-reading needed);search_filescaps results;run_bashtruncates stdout/stderr.
API References
main()
async
Run a small end-to-end task with the deep agent.
Source code in examples/20_deep_agent.py
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | |
populate_workspace(workdir)
Seed the workdir with a tiny Python project for the agent to work on.
Source code in examples/20_deep_agent.py
print_messages(result)
Pretty-print the agent's tool-call trajectory.
Source code in examples/20_deep_agent.py
Source
import asyncio
import os
import shutil
import tempfile
from dotenv import load_dotenv
import synalinks
# =============================================================================
# Workspace setup
# =============================================================================
def populate_workspace(workdir: str) -> None:
"""Seed the workdir with a tiny Python project for the agent to work on."""
os.makedirs(workdir, exist_ok=True)
# A starter module the agent will extend.
with open(os.path.join(workdir, "calculator.py"), "w") as f:
f.write(
"def add(a, b):\n"
" \"\"\"Return a + b.\"\"\"\n"
" return a + b\n"
)
# A README the agent can read to understand context.
with open(os.path.join(workdir, "README.md"), "w") as f:
f.write(
"# Calculator\n"
"\n"
"A tiny module. Currently supports `add(a, b)`.\n"
"Planned: `multiply(a, b)`.\n"
)
def print_messages(result) -> None:
"""Pretty-print the agent's tool-call trajectory."""
messages = result.get("messages", [])
for msg in messages:
role = msg.get("role")
if role == "assistant" and msg.get("tool_calls"):
for call in msg["tool_calls"]:
args = call.get("arguments", {})
args_str = ", ".join(f"{k}={v!r}" for k, v in args.items())
print(f" Tool call: {call['name']}({args_str})")
elif role == "tool":
content = msg.get("content", "")
if isinstance(content, dict):
content = str(content)
if len(content) > 200:
content = content[:200] + "..."
print(f" Tool result: {content}")
elif role == "assistant" and msg.get("content"):
print(f" Assistant: {msg['content']}")
# =============================================================================
# Main example
# =============================================================================
async def main():
"""Run a small end-to-end task with the deep agent."""
load_dotenv()
synalinks.clear_session()
workdir = tempfile.mkdtemp(prefix="deep_agent_demo_")
print(f"Workdir: {workdir}\n")
try:
populate_workspace(workdir)
lm = synalinks.LanguageModel(model="gemini/gemini-3.1-flash-lite-preview")
# Build the agent. autonomous=True runs the tool loop end-to-end;
# max_iterations=10 gives the LM enough rounds for a multi-step task.
inputs = synalinks.Input(data_model=synalinks.ChatMessages)
outputs = await synalinks.DeepAgent(
workdir=workdir,
language_model=lm,
max_iterations=10,
)(inputs)
agent = synalinks.Program(
inputs=inputs,
outputs=outputs,
name="deep_agent",
description="A coding agent with file and shell access.",
)
agent.summary()
# ---------------------------------------------------------------------
# Task 1: explore the workdir
# ---------------------------------------------------------------------
print("\n" + "=" * 60)
print("Task 1: Explore")
print("=" * 60)
question = synalinks.ChatMessages(
messages=[
synalinks.ChatMessage(
role="user",
content=(
"What's in this directory? List the files and tell "
"me what the project is about. Be brief."
),
)
]
)
result = await agent(question)
print_messages(result)
# ---------------------------------------------------------------------
# Task 2: extend the calculator
# ---------------------------------------------------------------------
print("\n" + "=" * 60)
print("Task 2: Extend & verify")
print("=" * 60)
task = synalinks.ChatMessages(
messages=[
synalinks.ChatMessage(
role="user",
content=(
"Open calculator.py and add a `multiply(a, b)` function "
"that returns a * b, in the same style as `add`. "
"Then run `python -c 'from calculator import multiply; "
"print(multiply(6, 7))'` and tell me what it printed."
),
)
]
)
result = await agent(task)
print_messages(result)
# Show the final file content so we can verify ourselves.
print("\n--- Final calculator.py ---")
with open(os.path.join(workdir, "calculator.py")) as f:
print(f.read())
finally:
shutil.rmtree(workdir, ignore_errors=True)
if __name__ == "__main__":
asyncio.run(main())