deepagents 0.3.7a1__py3-none-any.whl → 0.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents/backends/filesystem.py +55 -7
- deepagents/backends/sandbox.py +76 -23
- deepagents/graph.py +29 -10
- deepagents/middleware/__init__.py +3 -1
- deepagents/middleware/filesystem.py +508 -544
- deepagents/middleware/memory.py +11 -7
- deepagents/middleware/skills.py +4 -2
- deepagents/middleware/subagents.py +35 -19
- deepagents/middleware/summarization.py +763 -0
- {deepagents-0.3.7a1.dist-info → deepagents-0.3.9.dist-info}/METADATA +7 -7
- deepagents-0.3.9.dist-info/RECORD +22 -0
- {deepagents-0.3.7a1.dist-info → deepagents-0.3.9.dist-info}/WHEEL +1 -1
- deepagents-0.3.7a1.dist-info/RECORD +0 -21
- {deepagents-0.3.7a1.dist-info → deepagents-0.3.9.dist-info}/top_level.txt +0 -0
|
@@ -31,6 +31,39 @@ class FilesystemBackend(BackendProtocol):
|
|
|
31
31
|
Files are accessed using their actual filesystem paths. Relative paths are
|
|
32
32
|
resolved relative to the current working directory. Content is read/written
|
|
33
33
|
as plain text, and metadata (timestamps) are derived from filesystem stats.
|
|
34
|
+
|
|
35
|
+
!!! warning "Security Warning"
|
|
36
|
+
|
|
37
|
+
This backend grants agents direct filesystem read/write access. Use with
|
|
38
|
+
caution and only in appropriate environments.
|
|
39
|
+
|
|
40
|
+
**Appropriate use cases:**
|
|
41
|
+
|
|
42
|
+
- Local development CLIs (coding assistants, development tools)
|
|
43
|
+
- CI/CD pipelines (see security considerations below)
|
|
44
|
+
|
|
45
|
+
**Inappropriate use cases:**
|
|
46
|
+
|
|
47
|
+
- Web servers or HTTP APIs - use `StateBackend`, `StoreBackend`, or
|
|
48
|
+
`SandboxBackend` instead
|
|
49
|
+
|
|
50
|
+
**Security risks:**
|
|
51
|
+
|
|
52
|
+
- Agents can read any accessible file, including secrets (API keys,
|
|
53
|
+
credentials, `.env` files)
|
|
54
|
+
- Combined with network tools, secrets may be exfiltrated via SSRF attacks
|
|
55
|
+
- File modifications are permanent and irreversible
|
|
56
|
+
|
|
57
|
+
**Recommended safeguards:**
|
|
58
|
+
|
|
59
|
+
1. Enable Human-in-the-Loop (HITL) middleware to review sensitive operations
|
|
60
|
+
2. Exclude secrets from accessible filesystem paths (especially in CI/CD)
|
|
61
|
+
3. Use `SandboxBackend` for production environments requiring filesystem
|
|
62
|
+
interaction
|
|
63
|
+
4. **Always** use `virtual_mode=True` with `root_dir` to enable path-based
|
|
64
|
+
access restrictions (blocks `..`, `~`, and absolute paths outside root).
|
|
65
|
+
Note that the default (`virtual_mode=False`) provides no security even with
|
|
66
|
+
`root_dir` set.
|
|
34
67
|
"""
|
|
35
68
|
|
|
36
69
|
def __init__(
|
|
@@ -44,14 +77,29 @@ class FilesystemBackend(BackendProtocol):
|
|
|
44
77
|
Args:
|
|
45
78
|
root_dir: Optional root directory for file operations.
|
|
46
79
|
|
|
47
|
-
If provided,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
- If not provided, defaults to the current working directory.
|
|
81
|
+
- When `virtual_mode=False` (default): Only affects relative path
|
|
82
|
+
resolution. Provides **no security** - agents can access any file
|
|
83
|
+
using absolute paths or `..` sequences.
|
|
84
|
+
- When `virtual_mode=True`: All paths are restricted to this
|
|
85
|
+
directory with traversal protection enabled.
|
|
86
|
+
|
|
87
|
+
virtual_mode: Enable path-based access restrictions.
|
|
88
|
+
|
|
89
|
+
When `True`, all paths are treated as virtual paths anchored to
|
|
90
|
+
`root_dir`. Path traversal (`..`, `~`) is blocked and all resolved paths
|
|
91
|
+
are verified to remain within `root_dir`.
|
|
92
|
+
|
|
93
|
+
When `False` (default), **no security is provided**:
|
|
94
|
+
|
|
95
|
+
- Absolute paths (e.g., `/etc/passwd`) bypass `root_dir` entirely
|
|
96
|
+
- Relative paths with `..` can escape `root_dir`
|
|
97
|
+
- Agents have unrestricted filesystem access
|
|
98
|
+
|
|
99
|
+
**Security note:** `virtual_mode=True` provides path-based access
|
|
100
|
+
control, not process isolation. It restricts which files can be
|
|
101
|
+
accessed via paths, but does not sandbox the Python process itself.
|
|
51
102
|
|
|
52
|
-
Path traversal (using `..` or `~`) is disallowed and all resolved paths
|
|
53
|
-
must remain within the root directory. When `False` (default), absolute
|
|
54
|
-
paths are allowed as-is and relative paths resolve under cwd.
|
|
55
103
|
max_file_size_mb: Maximum file size in megabytes for operations like
|
|
56
104
|
grep's Python fallback search.
|
|
57
105
|
|
deepagents/backends/sandbox.py
CHANGED
|
@@ -46,12 +46,32 @@ for m in matches:
|
|
|
46
46
|
print(json.dumps(result))
|
|
47
47
|
" 2>/dev/null"""
|
|
48
48
|
|
|
49
|
+
# Use heredoc to pass content via stdin to avoid ARG_MAX limits on large files.
|
|
50
|
+
# ARG_MAX limits the total size of command-line arguments.
|
|
51
|
+
# Previously, base64-encoded content was interpolated directly into the command
|
|
52
|
+
# string, which would fail for files larger than ~100KB after base64 expansion.
|
|
53
|
+
# Heredocs bypass this by passing data through stdin rather than as arguments.
|
|
54
|
+
# Stdin format: first line is base64-encoded file path, second line is base64-encoded content.
|
|
49
55
|
_WRITE_COMMAND_TEMPLATE = """python3 -c "
|
|
50
56
|
import os
|
|
51
57
|
import sys
|
|
52
58
|
import base64
|
|
59
|
+
import json
|
|
53
60
|
|
|
54
|
-
file_path
|
|
61
|
+
# Read JSON payload from stdin containing file_path and content (both base64-encoded)
|
|
62
|
+
payload_b64 = sys.stdin.read().strip()
|
|
63
|
+
if not payload_b64:
|
|
64
|
+
print('Error: No payload received for write operation', file=sys.stderr)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
payload = base64.b64decode(payload_b64).decode('utf-8')
|
|
69
|
+
data = json.loads(payload)
|
|
70
|
+
file_path = data['path']
|
|
71
|
+
content = base64.b64decode(data['content']).decode('utf-8')
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f'Error: Failed to decode write payload: {e}', file=sys.stderr)
|
|
74
|
+
sys.exit(1)
|
|
55
75
|
|
|
56
76
|
# Check if file already exists (atomic with write)
|
|
57
77
|
if os.path.exists(file_path):
|
|
@@ -62,24 +82,46 @@ if os.path.exists(file_path):
|
|
|
62
82
|
parent_dir = os.path.dirname(file_path) or '.'
|
|
63
83
|
os.makedirs(parent_dir, exist_ok=True)
|
|
64
84
|
|
|
65
|
-
# Decode and write content
|
|
66
|
-
content = base64.b64decode('{content_b64}').decode('utf-8')
|
|
67
85
|
with open(file_path, 'w') as f:
|
|
68
86
|
f.write(content)
|
|
69
|
-
"
|
|
70
|
-
|
|
87
|
+
" <<'__DEEPAGENTS_EOF__'
|
|
88
|
+
{payload_b64}
|
|
89
|
+
__DEEPAGENTS_EOF__"""
|
|
90
|
+
|
|
91
|
+
# Use heredoc to pass edit parameters via stdin to avoid ARG_MAX limits.
|
|
92
|
+
# Stdin format: base64-encoded JSON with {"path": str, "old": str, "new": str}.
|
|
93
|
+
# JSON bundles all parameters; base64 ensures safe transport of arbitrary content
|
|
94
|
+
# (special chars, newlines, etc.) through the heredoc without escaping issues.
|
|
71
95
|
_EDIT_COMMAND_TEMPLATE = """python3 -c "
|
|
72
96
|
import sys
|
|
73
97
|
import base64
|
|
98
|
+
import json
|
|
99
|
+
import os
|
|
100
|
+
|
|
101
|
+
# Read and decode JSON payload from stdin
|
|
102
|
+
payload_b64 = sys.stdin.read().strip()
|
|
103
|
+
if not payload_b64:
|
|
104
|
+
print('Error: No payload received for edit operation', file=sys.stderr)
|
|
105
|
+
sys.exit(4)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
payload = base64.b64decode(payload_b64).decode('utf-8')
|
|
109
|
+
data = json.loads(payload)
|
|
110
|
+
file_path = data['path']
|
|
111
|
+
old = data['old']
|
|
112
|
+
new = data['new']
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f'Error: Failed to decode edit payload: {e}', file=sys.stderr)
|
|
115
|
+
sys.exit(4)
|
|
116
|
+
|
|
117
|
+
# Check if file exists
|
|
118
|
+
if not os.path.isfile(file_path):
|
|
119
|
+
sys.exit(3) # File not found
|
|
74
120
|
|
|
75
121
|
# Read file content
|
|
76
|
-
with open(
|
|
122
|
+
with open(file_path, 'r') as f:
|
|
77
123
|
text = f.read()
|
|
78
124
|
|
|
79
|
-
# Decode base64-encoded strings
|
|
80
|
-
old = base64.b64decode('{old_b64}').decode('utf-8')
|
|
81
|
-
new = base64.b64decode('{new_b64}').decode('utf-8')
|
|
82
|
-
|
|
83
125
|
# Count occurrences
|
|
84
126
|
count = text.count(old)
|
|
85
127
|
|
|
@@ -96,11 +138,13 @@ else:
|
|
|
96
138
|
result = text.replace(old, new, 1)
|
|
97
139
|
|
|
98
140
|
# Write back to file
|
|
99
|
-
with open(
|
|
141
|
+
with open(file_path, 'w') as f:
|
|
100
142
|
f.write(result)
|
|
101
143
|
|
|
102
144
|
print(count)
|
|
103
|
-
"
|
|
145
|
+
" <<'__DEEPAGENTS_EOF__'
|
|
146
|
+
{payload_b64}
|
|
147
|
+
__DEEPAGENTS_EOF__"""
|
|
104
148
|
|
|
105
149
|
_READ_COMMAND_TEMPLATE = """python3 -c "
|
|
106
150
|
import os
|
|
@@ -221,11 +265,14 @@ except PermissionError:
|
|
|
221
265
|
content: str,
|
|
222
266
|
) -> WriteResult:
|
|
223
267
|
"""Create a new file. Returns WriteResult; error populated on failure."""
|
|
224
|
-
#
|
|
268
|
+
# Create JSON payload with file path and base64-encoded content
|
|
269
|
+
# This avoids shell injection via file_path and ARG_MAX limits on content
|
|
225
270
|
content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
|
271
|
+
payload = json.dumps({"path": file_path, "content": content_b64})
|
|
272
|
+
payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
|
|
226
273
|
|
|
227
274
|
# Single atomic check + write command
|
|
228
|
-
cmd = _WRITE_COMMAND_TEMPLATE.format(
|
|
275
|
+
cmd = _WRITE_COMMAND_TEMPLATE.format(payload_b64=payload_b64)
|
|
229
276
|
result = self.execute(cmd)
|
|
230
277
|
|
|
231
278
|
# Check for errors (exit code or error message in output)
|
|
@@ -244,23 +291,29 @@ except PermissionError:
|
|
|
244
291
|
replace_all: bool = False,
|
|
245
292
|
) -> EditResult:
|
|
246
293
|
"""Edit a file by replacing string occurrences. Returns EditResult."""
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
# Create JSON payload with file path, old string, and new string
|
|
295
|
+
# This avoids shell injection via file_path and ARG_MAX limits on strings
|
|
296
|
+
payload = json.dumps({"path": file_path, "old": old_string, "new": new_string})
|
|
297
|
+
payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
|
|
250
298
|
|
|
251
299
|
# Use template for string replacement
|
|
252
|
-
cmd = _EDIT_COMMAND_TEMPLATE.format(
|
|
300
|
+
cmd = _EDIT_COMMAND_TEMPLATE.format(payload_b64=payload_b64, replace_all=replace_all)
|
|
253
301
|
result = self.execute(cmd)
|
|
254
302
|
|
|
255
303
|
exit_code = result.exit_code
|
|
256
304
|
output = result.output.strip()
|
|
257
305
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
306
|
+
# Map exit codes to error messages
|
|
307
|
+
error_messages = {
|
|
308
|
+
1: f"Error: String not found in file: '{old_string}'",
|
|
309
|
+
2: f"Error: String '{old_string}' appears multiple times. Use replace_all=True to replace all occurrences.",
|
|
310
|
+
3: f"Error: File '{file_path}' not found",
|
|
311
|
+
4: f"Error: Failed to decode edit payload: {output}",
|
|
312
|
+
}
|
|
313
|
+
if exit_code in error_messages:
|
|
314
|
+
return EditResult(error=error_messages[exit_code])
|
|
262
315
|
if exit_code != 0:
|
|
263
|
-
return EditResult(error=f"Error
|
|
316
|
+
return EditResult(error=f"Error editing file (exit code {exit_code}): {output or 'Unknown error'}")
|
|
264
317
|
|
|
265
318
|
count = int(output)
|
|
266
319
|
# External storage - no files_update needed
|
deepagents/graph.py
CHANGED
|
@@ -5,7 +5,6 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from langchain.agents import create_agent
|
|
7
7
|
from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig, TodoListMiddleware
|
|
8
|
-
from langchain.agents.middleware.summarization import SummarizationMiddleware
|
|
9
8
|
from langchain.agents.middleware.types import AgentMiddleware
|
|
10
9
|
from langchain.agents.structured_output import ResponseFormat
|
|
11
10
|
from langchain.chat_models import init_chat_model
|
|
@@ -26,6 +25,7 @@ from deepagents.middleware.memory import MemoryMiddleware
|
|
|
26
25
|
from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
|
|
27
26
|
from deepagents.middleware.skills import SkillsMiddleware
|
|
28
27
|
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
28
|
+
from deepagents.middleware.summarization import SummarizationMiddleware
|
|
29
29
|
|
|
30
30
|
BASE_AGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
|
|
31
31
|
|
|
@@ -38,7 +38,7 @@ def get_default_model() -> ChatAnthropic:
|
|
|
38
38
|
"""
|
|
39
39
|
return ChatAnthropic(
|
|
40
40
|
model_name="claude-sonnet-4-5-20250929",
|
|
41
|
-
max_tokens=20000,
|
|
41
|
+
max_tokens=20000, # type: ignore[call-arg]
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
|
|
@@ -63,11 +63,14 @@ def create_deep_agent(
|
|
|
63
63
|
) -> CompiledStateGraph:
|
|
64
64
|
"""Create a deep agent.
|
|
65
65
|
|
|
66
|
-
Deep agents require a LLM that supports tool calling
|
|
66
|
+
!!! warning "Deep agents require a LLM that supports tool calling!"
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
By default, this agent has access to the following tools:
|
|
69
|
+
|
|
70
|
+
- `write_todos`: manage a todo list
|
|
71
|
+
- `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`: file operations
|
|
72
|
+
- `execute`: run shell commands
|
|
73
|
+
- `task`: call subagents
|
|
71
74
|
|
|
72
75
|
The `execute` tool allows running shell commands if the backend implements `SandboxBackendProtocol`.
|
|
73
76
|
For non-sandbox backends, the `execute` tool will return an error message.
|
|
@@ -82,10 +85,14 @@ def create_deep_agent(
|
|
|
82
85
|
|
|
83
86
|
In addition to custom tools you provide, deep agents include built-in tools for planning,
|
|
84
87
|
file management, and subagent spawning.
|
|
85
|
-
system_prompt:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
system_prompt: Custom system instructions to prepend before the base deep agent
|
|
89
|
+
prompt.
|
|
90
|
+
|
|
91
|
+
If a string, it's concatenated with the base prompt.
|
|
92
|
+
middleware: Additional middleware to apply after the standard middleware stack
|
|
93
|
+
(`TodoListMiddleware`, `FilesystemMiddleware`, `SubAgentMiddleware`,
|
|
94
|
+
`SummarizationMiddleware`, `AnthropicPromptCachingMiddleware`,
|
|
95
|
+
`PatchToolCallsMiddleware`).
|
|
89
96
|
subagents: The subagents to use.
|
|
90
97
|
|
|
91
98
|
Each subagent should be a `dict` with the following keys:
|
|
@@ -142,9 +149,17 @@ def create_deep_agent(
|
|
|
142
149
|
):
|
|
143
150
|
trigger = ("fraction", 0.85)
|
|
144
151
|
keep = ("fraction", 0.10)
|
|
152
|
+
truncate_args_settings = {
|
|
153
|
+
"trigger": ("fraction", 0.85),
|
|
154
|
+
"keep": ("fraction", 0.10),
|
|
155
|
+
}
|
|
145
156
|
else:
|
|
146
157
|
trigger = ("tokens", 170000)
|
|
147
158
|
keep = ("messages", 6)
|
|
159
|
+
truncate_args_settings = {
|
|
160
|
+
"trigger": ("messages", 20),
|
|
161
|
+
"keep": ("messages", 20),
|
|
162
|
+
}
|
|
148
163
|
|
|
149
164
|
# Build middleware stack for subagents (includes skills if provided)
|
|
150
165
|
subagent_middleware: list[AgentMiddleware] = [
|
|
@@ -160,9 +175,11 @@ def create_deep_agent(
|
|
|
160
175
|
FilesystemMiddleware(backend=backend),
|
|
161
176
|
SummarizationMiddleware(
|
|
162
177
|
model=model,
|
|
178
|
+
backend=backend,
|
|
163
179
|
trigger=trigger,
|
|
164
180
|
keep=keep,
|
|
165
181
|
trim_tokens_to_summarize=None,
|
|
182
|
+
truncate_args_settings=truncate_args_settings,
|
|
166
183
|
),
|
|
167
184
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
168
185
|
PatchToolCallsMiddleware(),
|
|
@@ -190,9 +207,11 @@ def create_deep_agent(
|
|
|
190
207
|
),
|
|
191
208
|
SummarizationMiddleware(
|
|
192
209
|
model=model,
|
|
210
|
+
backend=backend,
|
|
193
211
|
trigger=trigger,
|
|
194
212
|
keep=keep,
|
|
195
213
|
trim_tokens_to_summarize=None,
|
|
214
|
+
truncate_args_settings=truncate_args_settings,
|
|
196
215
|
),
|
|
197
216
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
198
217
|
PatchToolCallsMiddleware(),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
"""Middleware for the
|
|
1
|
+
"""Middleware for the agent."""
|
|
2
2
|
|
|
3
3
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
4
4
|
from deepagents.middleware.memory import MemoryMiddleware
|
|
5
5
|
from deepagents.middleware.skills import SkillsMiddleware
|
|
6
6
|
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
7
|
+
from deepagents.middleware.summarization import SummarizationMiddleware
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"CompiledSubAgent",
|
|
@@ -12,4 +13,5 @@ __all__ = [
|
|
|
12
13
|
"SkillsMiddleware",
|
|
13
14
|
"SubAgent",
|
|
14
15
|
"SubAgentMiddleware",
|
|
16
|
+
"SummarizationMiddleware",
|
|
15
17
|
]
|